UNPKG

@bomb.sh/tools

Version:

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

159 lines (158 loc) 5.54 kB
import { relativeUrlPath, resolveLinkTarget } from "../utils.mjs"; import { findPackageJSON } from "node:module"; import { cwd, env, platform } from "node:process"; import { fileURLToPath, pathToFileURL } from "node:url"; import { readlink, rm, symlink } from "node:fs/promises"; import { NodeHfs } from "@humanfs/node"; import { parse } from "ultramatter"; //#region src/commands/sync.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"; async function sync(_ctx) { 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 = 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/`); } async function copySkills(options) { const { source, dest } = options; const skills = []; const keep = /* @__PURE__ */ new Set(); 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); const linkPath = fileURLToPath(new URL(name, dest)); await rm(linkPath, { recursive: true, force: true }); await symlink(relativeUrlPath(dest, srcDir), 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) { const { dest, source, keep } = options; if (!await hfs.isDirectory(dest)) return; 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 { if (resolveLinkTarget(dest, await readlink(linkPath)).href.startsWith(source.href)) await hfs.deleteAll(linkPath); } catch {} } } async function updateGitignore(options) { const { root, skills } = options; const gitignorePath = new URL(".gitignore", root); let content = await hfs.text(gitignorePath) ?? ""; const section = [ GITIGNORE_START, ...skills.map((s) => `skills/${s.name}/`), 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 + 13); else if (skills.length > 0) { const suffix = content.endsWith("\n") || content === "" ? "" : "\n"; content = content + suffix + "\n" + section + "\n"; } await hfs.write(gitignorePath, content); } async function updateAgentsMd(options) { const { root, skills } = options; const agentsPath = new URL("AGENTS.md", root); let content = await hfs.text(agentsPath) ?? ""; const section = [ SENTINEL_START, "## @bomb.sh/tools Skills", "", "When working on these tasks, read the linked skill file for guidance:", "", ...skills.map((s) => { const desc = s.description.split(".")[0]?.trim(); return `- **${s.name}** — [skills/${s.name}/SKILL.md](skills/${s.name}/SKILL.md)${desc ? ` - ${desc}` : ""}`; }), 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 + 20); else { const suffix = content.endsWith("\n") || content === "" ? "" : "\n"; content = content + suffix + "\n" + section + "\n"; } await hfs.write(agentsPath, content); } function parseFrontmatter(content) { const { frontmatter } = parse(content); if (!frontmatter) return void 0; const name = frontmatter.name; const description = frontmatter.description?.trim().replaceAll(/\s+/g, " ") ?? ""; if (!name) return void 0; 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. */ async function findParentPackage() { const candidate = findPackageJSON(pathToFileURL(`${env.INIT_CWD ?? cwd()}/`)); if (!candidate) return null; const text = await hfs.text(pathToFileURL(candidate)); if (!text) return null; try { if (JSON.parse(text).name === "@bomb.sh/tools") return null; } catch { return null; } return candidate; } //#endregion export { copySkills, findParentPackage, sync }; //# sourceMappingURL=sync.mjs.map