[READ-ONLY] Mirror of https://github.com/bombshell-dev/tools. Internal CLI to standardize tooling across all Bombshell projects
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213import { readlink, realpath, rm, symlink } from 'node:fs/promises';import { findPackageJSON } from 'node:module';import { cwd, env, platform } from 'node:process';import { fileURLToPath, pathToFileURL } from 'node:url';import { NodeHfs } from '@humanfs/node';import { parse } from 'yaml';import type { CommandContext } from '../context.ts';import { relativeUrlPath, resolveLinkTarget } from '../utils.ts';
const hfs = new NodeHfs();
const SENTINEL_START = '<!-- bsh:skills -->';const SENTINEL_END = '<!-- /bsh:skills -->';const GITIGNORE_START = '# bsh:skills';const GITIGNORE_END = '# /bsh:skills';
export async function sync(_ctx: CommandContext): Promise<void> { const parentPkg = await findParentPackage(); if (!parentPkg) { console.info('Skipping sync — no parent project found (running inside @bomb.sh/tools?)'); return; }
const root = new URL('./', pathToFileURL(parentPkg)); const source = await resolveSkillsSource(root, new URL('../../skills/', import.meta.url));
if (!(await hfs.isDirectory(source))) { console.error('Could not locate bundled skills directory.'); return; }
const skills = await copySkills({ source, dest: new URL('skills/', root) }); await updateGitignore({ root, skills }); await updateAgentsMd({ root, skills });
console.info(`Synced ${skills.length} skills to skills/`);}
/** * Prefer linking through the project's `node_modules/@bomb.sh/tools` over * `import.meta.url`, which resolves to the real path. Under pnpm that is a * versioned `node_modules/.pnpm/<hash>/` directory, so links into it dangle * once a reinstall changes the hash. */export async function resolveSkillsSource(root: URL, fallback: URL): Promise<URL> { const linked = new URL('node_modules/@bomb.sh/tools/skills/', root); return (await hfs.isDirectory(linked)) ? linked : fallback;}
interface SkillInfo { name: string; description: string;}
export async function copySkills(options: { source: URL; dest: URL }): Promise<SkillInfo[]> { const { source, dest } = options; const skills: SkillInfo[] = [];
const keep = new Set<string>(); for await (const entry of hfs.list(source)) { if (entry.isDirectory && !entry.name.startsWith('_')) { keep.add(entry.name); } }
await hfs.createDirectory(dest); await pruneStaleLinks({ dest, source, keep });
const linkType = platform === 'win32' ? 'junction' : 'dir';
for (const name of keep) { const srcDir = new URL(`${name}/`, source);
// Use a path without a trailing slash. macOS rejects a trailing-slash link // path with ENOENT, and `rm` on a trailing-slash directory symlink follows // the link and deletes the source rather than unlinking the symlink itself. const linkPath = fileURLToPath(new URL(name, dest)); await rm(linkPath, { recursive: true, force: true });
const target = relativeUrlPath(dest, srcDir); await symlink(target, linkPath, linkType);
const content = await hfs.text(new URL('SKILL.md', srcDir)); if (content) { const frontmatter = parseFrontmatter(content); if (frontmatter) { skills.push(frontmatter); } } }
return skills;}
async function pruneStaleLinks(options: { dest: URL; source: URL; keep: Set<string>;}): Promise<void> { const { dest, source, keep } = options; if (!(await hfs.isDirectory(dest))) return;
// Links from older syncs may target the real path rather than `source`. const sources = [source.href, `${pathToFileURL(await realpath(source)).href}/`];
for await (const entry of hfs.list(dest)) { if (!entry.isSymlink) continue; if (keep.has(entry.name)) continue;
const linkPath = fileURLToPath(new URL(entry.name, dest)); try { const { href } = resolveLinkTarget(dest, await readlink(linkPath)); if (sources.some((s) => href.startsWith(s))) { await hfs.deleteAll(linkPath); } } catch { // ignore unreadable links } }}
async function updateGitignore(options: { root: URL; skills: SkillInfo[] }): Promise<void> { const { root, skills } = options; const gitignorePath = new URL('.gitignore', root); let content = (await hfs.text(gitignorePath)) ?? '';
const lines = skills.map((s) => `skills/${s.name}/`); const section = [GITIGNORE_START, ...lines, GITIGNORE_END].join('\n');
const startIdx = content.indexOf(GITIGNORE_START); const endIdx = content.indexOf(GITIGNORE_END);
if (startIdx !== -1 && endIdx !== -1) { content = content.slice(0, startIdx) + section + content.slice(endIdx + GITIGNORE_END.length); } else if (skills.length > 0) { const suffix = content.endsWith('\n') || content === '' ? '' : '\n'; content = content + suffix + '\n' + section + '\n'; }
await hfs.write(gitignorePath, content);}
export async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Promise<void> { const { root, skills } = options; const agentsPath = new URL('AGENTS.md', root); let content = (await hfs.text(agentsPath)) ?? '';
const lines = skills.map((s) => { const desc = s.description.split(/\.(?:\s|$)/)[0]?.trim(); return `- **${s.name}** — [skills/${s.name}/SKILL.md](skills/${s.name}/SKILL.md)${desc ? ` - ${desc}` : ''}`; });
const section = [ SENTINEL_START, '## @bomb.sh/tools Skills', '', 'When working on these tasks, read the linked skill file for guidance:', '', ...lines, SENTINEL_END, ].join('\n');
const startIdx = content.indexOf(SENTINEL_START); const endIdx = content.indexOf(SENTINEL_END);
if (startIdx !== -1 && endIdx !== -1) { content = content.slice(0, startIdx) + section + content.slice(endIdx + SENTINEL_END.length); } else { const suffix = content.endsWith('\n') || content === '' ? '' : '\n'; content = content + suffix + '\n' + section + '\n'; }
await hfs.write(agentsPath, content);}
function parseFrontmatter(content: string): SkillInfo | undefined { const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content); if (!match) return undefined; const frontmatter = parse(match[1]!) as Record<string, unknown> | null; if (!frontmatter) return undefined; const name = frontmatter.name as string | undefined; const description = (frontmatter.description as string | undefined)?.trim().replaceAll(/\s+/g, ' ') ?? ''; if (!name) return undefined; return { name, description };}
/** * Locate the consuming project's package.json. The project root must come * from where the command was invoked, never from this package's physical * location: under pnpm's isolated layout, import.meta.url resolves through * the node_modules symlink into node_modules/.pnpm/<hash>/, and walking up * from there lands in the store, not the user's project. INIT_CWD (set by * pnpm/npm to the directory the script was run from) is preferred because * package scripts may rewrite cwd. Returns null when no project is found or * when invoked inside @bomb.sh/tools itself. */export async function findParentPackage(): Promise<string | null> { const startDir = env.INIT_CWD ?? cwd(); const candidate = findPackageJSON(pathToFileURL(`${startDir}/`)); if (!candidate) return null;
const text = await hfs.text(pathToFileURL(candidate)); if (!text) return null; try { const pkg = JSON.parse(text) as { name?: string }; if (pkg.name === '@bomb.sh/tools') return null; } catch { return null; } return candidate;}