@bomb.sh/tools
Version:
The internal dev, build, and lint CLI for Bombshell projects
1 lines • 10.3 kB
Source Map (JSON)
{"version":3,"file":"sync.mjs","names":[],"sources":["../../src/commands/sync.ts"],"sourcesContent":["import { readlink, 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 'ultramatter';\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';\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 = 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/', root) });\n\tawait updateGitignore({ root, skills });\n\tawait updateAgentsMd({ root, skills });\n\n\tconsole.info(`Synced ${skills.length} skills to skills/`);\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\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 target = await readlink(linkPath);\n\t\t\tif (resolveLinkTarget(dest, target).href.startsWith(source.href)) {\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\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/${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\nasync 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('.')[0]?.trim();\n\t\treturn `- **${s.name}** — [skills/${s.name}/SKILL.md](skills/${s.name}/SKILL.md)${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 { frontmatter } = parse(content);\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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"}