UNPKG

@bomb.sh/tools

Version:

The internal dev, build, and lint CLI for Bombshell projects

1 lines • 12.9 kB
{"version":3,"file":"sync.mjs","names":[],"sources":["../../src/commands/sync.ts"],"sourcesContent":["import { readlink, realpath, rm, symlink } from 'node:fs/promises';\nimport { findPackageJSON } from 'node:module';\nimport { cwd, env, platform } from 'node:process';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { NodeHfs } from '@humanfs/node';\nimport { parse } from 'yaml';\nimport type { CommandContext } from '../context.ts';\nimport { relativeUrlPath, resolveLinkTarget } from '../utils.ts';\n\nconst hfs = new NodeHfs();\n\nconst SENTINEL_START = '<!-- bsh:skills -->';\nconst SENTINEL_END = '<!-- /bsh:skills -->';\nconst GITIGNORE_START = '# bsh:skills';\nconst GITIGNORE_END = '# /bsh:skills';\nconst SKILLS_DIR = '.agents/skills/';\n// Where syncs before `.agents/skills/` linked skills.\nconst LEGACY_SKILLS_DIR = 'skills/';\n\nexport async function sync(_ctx: CommandContext): Promise<void> {\n\tconst parentPkg = await findParentPackage();\n\tif (!parentPkg) {\n\t\tconsole.info('Skipping sync — no parent project found (running inside @bomb.sh/tools?)');\n\t\treturn;\n\t}\n\n\tconst root = new URL('./', pathToFileURL(parentPkg));\n\tconst source = await resolveSkillsSource(root, new URL('../../skills/', import.meta.url));\n\n\tif (!(await hfs.isDirectory(source))) {\n\t\tconsole.error('Could not locate bundled skills directory.');\n\t\treturn;\n\t}\n\n\tconst skills = await copySkills({ source, dest: new URL(SKILLS_DIR, root) });\n\tawait removeLegacyLinks({ dir: new URL(LEGACY_SKILLS_DIR, root), skills });\n\tawait updateGitignore({ root, skills });\n\tawait updateAgentsMd({ root, skills });\n\n\tconsole.info(`Synced ${skills.length} skills to ${SKILLS_DIR}`);\n}\n\n/**\n * Prefer linking through the project's `node_modules/@bomb.sh/tools` over\n * `import.meta.url`, which resolves to the real path. Under pnpm that is a\n * versioned `node_modules/.pnpm/<hash>/` directory, so links into it dangle\n * once a reinstall changes the hash.\n */\nexport async function resolveSkillsSource(root: URL, fallback: URL): Promise<URL> {\n\tconst linked = new URL('node_modules/@bomb.sh/tools/skills/', root);\n\treturn (await hfs.isDirectory(linked)) ? linked : fallback;\n}\n\ninterface SkillInfo {\n\tname: string;\n\tdescription: string;\n}\n\nexport async function copySkills(options: { source: URL; dest: URL }): Promise<SkillInfo[]> {\n\tconst { source, dest } = options;\n\tconst skills: SkillInfo[] = [];\n\n\tconst keep = new Set<string>();\n\tfor await (const entry of hfs.list(source)) {\n\t\tif (entry.isDirectory && !entry.name.startsWith('_')) {\n\t\t\tkeep.add(entry.name);\n\t\t}\n\t}\n\n\tawait hfs.createDirectory(dest);\n\tawait pruneStaleLinks({ dest, source, keep });\n\n\tconst linkType = platform === 'win32' ? 'junction' : 'dir';\n\n\tfor (const name of keep) {\n\t\tconst srcDir = new URL(`${name}/`, source);\n\n\t\t// Use a path without a trailing slash. macOS rejects a trailing-slash link\n\t\t// path with ENOENT, and `rm` on a trailing-slash directory symlink follows\n\t\t// the link and deletes the source rather than unlinking the symlink itself.\n\t\tconst linkPath = fileURLToPath(new URL(name, dest));\n\t\tawait rm(linkPath, { recursive: true, force: true });\n\n\t\tconst target = relativeUrlPath(dest, srcDir);\n\t\tawait symlink(target, linkPath, linkType);\n\n\t\tconst content = await hfs.text(new URL('SKILL.md', srcDir));\n\t\tif (content) {\n\t\t\tconst frontmatter = parseFrontmatter(content);\n\t\t\tif (frontmatter) {\n\t\t\t\tskills.push(frontmatter);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn skills;\n}\n\nasync function pruneStaleLinks(options: {\n\tdest: URL;\n\tsource: URL;\n\tkeep: Set<string>;\n}): Promise<void> {\n\tconst { dest, source, keep } = options;\n\tif (!(await hfs.isDirectory(dest))) return;\n\n\t// Links from older syncs may target the real path rather than `source`.\n\tconst sources = [source.href, `${pathToFileURL(await realpath(source)).href}/`];\n\n\tfor await (const entry of hfs.list(dest)) {\n\t\tif (!entry.isSymlink) continue;\n\t\tif (keep.has(entry.name)) continue;\n\n\t\tconst linkPath = fileURLToPath(new URL(entry.name, dest));\n\t\ttry {\n\t\t\tconst { href } = resolveLinkTarget(dest, await readlink(linkPath));\n\t\t\tif (sources.some((s) => href.startsWith(s))) {\n\t\t\t\tawait hfs.deleteAll(linkPath);\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore unreadable links\n\t\t}\n\t}\n}\n\n/**\n * Remove links from syncs that predate `.agents/skills/`. Match by name, not\n * target: after an upgrade those links point into a store path that may no\n * longer exist. Anything that isn't a symlink is left alone.\n */\nasync function removeLegacyLinks(options: { dir: URL; skills: SkillInfo[] }): Promise<void> {\n\tconst { dir, skills } = options;\n\tif (!(await hfs.isDirectory(dir))) return;\n\n\tconst names = new Set(skills.map((s) => s.name));\n\tlet remaining = 0;\n\tfor await (const entry of hfs.list(dir)) {\n\t\tif (entry.isSymlink && names.has(entry.name)) {\n\t\t\tawait rm(fileURLToPath(new URL(entry.name, dir)));\n\t\t} else {\n\t\t\tremaining++;\n\t\t}\n\t}\n\tif (remaining === 0) await hfs.delete(dir);\n}\n\nasync function updateGitignore(options: { root: URL; skills: SkillInfo[] }): Promise<void> {\n\tconst { root, skills } = options;\n\tconst gitignorePath = new URL('.gitignore', root);\n\tlet content = (await hfs.text(gitignorePath)) ?? '';\n\n\tconst lines = skills.map((s) => `${SKILLS_DIR}${s.name}/`);\n\tconst section = [GITIGNORE_START, ...lines, GITIGNORE_END].join('\\n');\n\n\tconst startIdx = content.indexOf(GITIGNORE_START);\n\tconst endIdx = content.indexOf(GITIGNORE_END);\n\n\tif (startIdx !== -1 && endIdx !== -1) {\n\t\tcontent = content.slice(0, startIdx) + section + content.slice(endIdx + GITIGNORE_END.length);\n\t} else if (skills.length > 0) {\n\t\tconst suffix = content.endsWith('\\n') || content === '' ? '' : '\\n';\n\t\tcontent = content + suffix + '\\n' + section + '\\n';\n\t}\n\n\tawait hfs.write(gitignorePath, content);\n}\n\nexport async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Promise<void> {\n\tconst { root, skills } = options;\n\tconst agentsPath = new URL('AGENTS.md', root);\n\tlet content = (await hfs.text(agentsPath)) ?? '';\n\n\tconst lines = skills.map((s) => {\n\t\tconst desc = s.description.split(/\\.(?:\\s|$)/)[0]?.trim();\n\t\tconst path = `${SKILLS_DIR}${s.name}/SKILL.md`;\n\t\treturn `- **${s.name}** — [${path}](${path})${desc ? ` - ${desc}` : ''}`;\n\t});\n\n\tconst section = [\n\t\tSENTINEL_START,\n\t\t'## @bomb.sh/tools Skills',\n\t\t'',\n\t\t'When working on these tasks, read the linked skill file for guidance:',\n\t\t'',\n\t\t...lines,\n\t\tSENTINEL_END,\n\t].join('\\n');\n\n\tconst startIdx = content.indexOf(SENTINEL_START);\n\tconst endIdx = content.indexOf(SENTINEL_END);\n\n\tif (startIdx !== -1 && endIdx !== -1) {\n\t\tcontent = content.slice(0, startIdx) + section + content.slice(endIdx + SENTINEL_END.length);\n\t} else {\n\t\tconst suffix = content.endsWith('\\n') || content === '' ? '' : '\\n';\n\t\tcontent = content + suffix + '\\n' + section + '\\n';\n\t}\n\n\tawait hfs.write(agentsPath, content);\n}\n\nfunction parseFrontmatter(content: string): SkillInfo | undefined {\n\tconst match = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---/.exec(content);\n\tif (!match) return undefined;\n\tconst frontmatter = parse(match[1]!) as Record<string, unknown> | null;\n\tif (!frontmatter) return undefined;\n\tconst name = frontmatter.name as string | undefined;\n\tconst description =\n\t\t(frontmatter.description as string | undefined)?.trim().replaceAll(/\\s+/g, ' ') ?? '';\n\tif (!name) return undefined;\n\treturn { name, description };\n}\n\n/**\n * Locate the consuming project's package.json. The project root must come\n * from where the command was invoked, never from this package's physical\n * location: under pnpm's isolated layout, import.meta.url resolves through\n * the node_modules symlink into node_modules/.pnpm/<hash>/, and walking up\n * from there lands in the store, not the user's project. INIT_CWD (set by\n * pnpm/npm to the directory the script was run from) is preferred because\n * package scripts may rewrite cwd. Returns null when no project is found or\n * when invoked inside @bomb.sh/tools itself.\n */\nexport async function findParentPackage(): Promise<string | null> {\n\tconst startDir = env.INIT_CWD ?? cwd();\n\tconst candidate = findPackageJSON(pathToFileURL(`${startDir}/`));\n\tif (!candidate) return null;\n\n\tconst text = await hfs.text(pathToFileURL(candidate));\n\tif (!text) return null;\n\ttry {\n\t\tconst pkg = JSON.parse(text) as { name?: string };\n\t\tif (pkg.name === '@bomb.sh/tools') return null;\n\t} catch {\n\t\treturn null;\n\t}\n\treturn candidate;\n}\n"],"mappings":"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"}