[READ-ONLY] Mirror of https://github.com/bombshell-dev/tools. Internal CLI to standardize tooling across all Bombshell projects
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(sync): refactor sync command

Nate Moore (May 7, 2026, 9:56 PM EDT) 340e898f 1ac89a11

+49 -46
+5
.changeset/easy-doodles-make.md
··· 1 + --- 2 + "@bomb.sh/tools": patch 3 + --- 4 + 5 + Fixes errors that caused bsh sync to crash
+44 -46
src/commands/sync.ts
··· 1 - import { existsSync, type Dirent } from 'node:fs'; 2 - import { mkdir, readdir, readFile, readlink, rm, symlink, writeFile } from 'node:fs/promises'; 3 - import { isAbsolute, relative, resolve } from 'node:path'; 4 - import { cwd, platform } from 'node:process'; 1 + import { readlink, symlink } from 'node:fs/promises'; 2 + import { findPackageJSON } from 'node:module'; 3 + import { dirname, isAbsolute, relative, resolve } from 'node:path'; 4 + import { platform } from 'node:process'; 5 5 import { fileURLToPath, pathToFileURL } from 'node:url'; 6 + import { NodeHfs } from '@humanfs/node'; 6 7 import { parse } from 'ultramatter'; 7 8 import type { CommandContext } from '../context.ts'; 8 9 10 + const hfs = new NodeHfs(); 11 + 9 12 const SENTINEL_START = '<!-- bsh:skills -->'; 10 13 const SENTINEL_END = '<!-- /bsh:skills -->'; 11 14 const GITIGNORE_START = '# bsh:skills'; 12 15 const GITIGNORE_END = '# /bsh:skills'; 13 16 14 17 export async function sync(_ctx: CommandContext): Promise<void> { 15 - const root = pathToFileURL(`${cwd()}/`); 16 - 17 - if (await isSelf(root)) { 18 - console.info('Skipping sync — running inside @bomb.sh/tools'); 18 + const parentPkg = findParentPackage(); 19 + if (!parentPkg) { 20 + console.info('Skipping sync — no parent project found (running inside @bomb.sh/tools?)'); 19 21 return; 20 22 } 21 23 22 - const source = new URL('node_modules/@bomb.sh/tools/skills/', root); 23 - if (!existsSync(source)) { 24 - console.error('@bomb.sh/tools is not installed. Run `pnpm add -D @bomb.sh/tools` first.'); 24 + const root = pathToFileURL(`${dirname(parentPkg)}/`); 25 + const source = new URL('../../skills/', import.meta.url); 26 + 27 + if (!(await hfs.isDirectory(source))) { 28 + console.error('Could not locate bundled skills directory.'); 25 29 return; 26 30 } 27 31 ··· 40 44 async function copySkills(options: { source: URL; dest: URL }): Promise<SkillInfo[]> { 41 45 const { source, dest } = options; 42 46 const skills: SkillInfo[] = []; 43 - const entries = await readdir(source, { withFileTypes: true }); 44 - const keep = new Set<string>( 45 - entries.filter((e) => e.isDirectory() && !e.name.startsWith('_')).map((e) => e.name), 46 - ); 47 47 48 - await mkdir(dest, { recursive: true }); 48 + const keep = new Set<string>(); 49 + for await (const entry of hfs.list(source)) { 50 + if (entry.isDirectory && !entry.name.startsWith('_')) { 51 + keep.add(entry.name); 52 + } 53 + } 54 + 55 + await hfs.createDirectory(dest); 49 56 await pruneStaleLinks({ dest, source, keep }); 50 57 51 58 const destDirPath = fileURLToPath(dest); ··· 55 62 const srcDir = new URL(`${name}/`, source); 56 63 const destDir = new URL(`${name}/`, dest); 57 64 58 - await rm(destDir, { recursive: true, force: true }); 65 + await hfs.deleteAll(destDir); 59 66 60 67 const target = relative(destDirPath, fileURLToPath(srcDir)); 61 68 await symlink(target, fileURLToPath(destDir), linkType); 62 69 63 - const skillMd = new URL('SKILL.md', srcDir); 64 - 65 - const content = await safeRead(skillMd); 70 + const content = await hfs.text(new URL('SKILL.md', srcDir)); 66 71 if (content) { 67 72 const frontmatter = parseFrontmatter(content); 68 73 if (frontmatter) { ··· 80 85 keep: Set<string>; 81 86 }): Promise<void> { 82 87 const { dest, source, keep } = options; 88 + if (!(await hfs.isDirectory(dest))) return; 89 + 83 90 const destPath = fileURLToPath(dest); 84 91 const sourcePath = fileURLToPath(source); 85 92 86 - let entries: Dirent[]; 87 - try { 88 - entries = await readdir(dest, { withFileTypes: true }); 89 - } catch { 90 - return; 91 - } 92 - 93 - for (const entry of entries) { 94 - if (!entry.isSymbolicLink()) continue; 93 + for await (const entry of hfs.list(dest)) { 94 + if (!entry.isSymlink) continue; 95 95 if (keep.has(entry.name)) continue; 96 96 97 97 const linkPath = fileURLToPath(new URL(entry.name, dest)); ··· 99 99 const target = await readlink(linkPath); 100 100 const absTarget = isAbsolute(target) ? target : resolve(destPath, target); 101 101 if (absTarget.startsWith(sourcePath)) { 102 - await rm(linkPath, { recursive: true, force: true }); 102 + await hfs.deleteAll(linkPath); 103 103 } 104 104 } catch { 105 105 // ignore unreadable links ··· 110 110 async function updateGitignore(options: { root: URL; skills: SkillInfo[] }): Promise<void> { 111 111 const { root, skills } = options; 112 112 const gitignorePath = new URL('.gitignore', root); 113 - let content = (await safeRead(gitignorePath)) ?? ''; 113 + let content = (await hfs.text(gitignorePath)) ?? ''; 114 114 115 115 const lines = skills.map((s) => `skills/${s.name}/`); 116 116 const section = [GITIGNORE_START, ...lines, GITIGNORE_END].join('\n'); ··· 125 125 content = content + suffix + '\n' + section + '\n'; 126 126 } 127 127 128 - await writeFile(gitignorePath, content, 'utf8'); 128 + await hfs.write(gitignorePath, content); 129 129 } 130 130 131 131 async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Promise<void> { 132 132 const { root, skills } = options; 133 133 const agentsPath = new URL('AGENTS.md', root); 134 - let content = (await safeRead(agentsPath)) ?? ''; 134 + let content = (await hfs.text(agentsPath)) ?? ''; 135 135 136 136 const lines = skills.map((s) => { 137 137 const desc = s.description.split('.')[0]?.trim(); ··· 158 158 content = content + suffix + '\n' + section + '\n'; 159 159 } 160 160 161 - await writeFile(agentsPath, content, 'utf8'); 161 + await hfs.write(agentsPath, content); 162 162 } 163 163 164 164 function parseFrontmatter(content: string): SkillInfo | undefined { ··· 171 171 return { name, description }; 172 172 } 173 173 174 - async function isSelf(root: URL): Promise<boolean> { 175 - const content = await safeRead(new URL('package.json', root)); 176 - if (!content) return false; 177 - const pkg = JSON.parse(content) as { name?: string }; 178 - return pkg.name === '@bomb.sh/tools'; 179 - } 174 + function findParentPackage(): string | null { 175 + const ownPkg = findPackageJSON(import.meta.url); 176 + if (!ownPkg) return null; 180 177 181 - async function safeRead(url: URL): Promise<string | null> { 182 - try { 183 - return await readFile(url, 'utf8'); 184 - } catch (err) { 185 - if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') return null; 186 - throw err; 178 + let cursor = dirname(dirname(ownPkg)); 179 + while (cursor !== dirname(cursor)) { 180 + const candidate = findPackageJSON(pathToFileURL(`${cursor}/`)); 181 + if (!candidate) return null; 182 + if (candidate !== ownPkg) return candidate; 183 + cursor = dirname(dirname(candidate)); 187 184 } 185 + return null; 188 186 }