@bomb.sh/tools
Version:
The internal dev, build, and lint CLI for Bombshell projects
159 lines (158 loc) • 5.54 kB
JavaScript
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