[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: resolve the sync target from the invocation directory, not the package location (#46)

authored by

Nate Moore and committed by
GitHub
(Jul 16, 2026, 11:39 PM EDT) 08e749c1 b19d89a0

+55 -14
+4
.changeset/pnpm-sync-project-root.md
··· 1 + --- 2 + '@bomb.sh/tools': patch 3 + --- 4 + Fixes `bsh sync` writing its output into the pnpm store instead of the project root. The project is now resolved from the invocation directory (`INIT_CWD`, falling back to `cwd`), so `skills/` symlinks, the `AGENTS.md` section, and the `.gitignore` entries land in the project that ran the command under pnpm's default isolated `node_modules` layout
+27 -2
src/commands/sync.test.ts
··· 1 1 import { lstat, readlink } from 'node:fs/promises'; 2 2 import { fileURLToPath } from 'node:url'; 3 3 import { describe, it, expect } from 'vitest'; 4 - import { createFixture } from '../test-utils/index.ts'; 5 - import { copySkills } from './sync.ts'; 4 + import { createFixture, createMocks } from '../test-utils/index.ts'; 5 + import { copySkills, findParentPackage } from './sync.ts'; 6 6 7 7 describe('copySkills', () => { 8 8 it('symlinks each skill into the destination', async () => { ··· 54 54 expect(await fixture.text('project/skills/build/SKILL.md')).toContain('name: build'); 55 55 }); 56 56 }); 57 + 58 + describe('findParentPackage', () => { 59 + it('resolves the project root from INIT_CWD, not from this package', async () => { 60 + const fixture = await createFixture({ 61 + project: { 62 + 'package.json': '{ "name": "my-app" }', 63 + nested: {}, 64 + }, 65 + }); 66 + createMocks({ env: { INIT_CWD: fileURLToPath(new URL('project/nested/', fixture.root)) } }); 67 + 68 + const found = await findParentPackage(); 69 + 70 + expect(found).toBe(fileURLToPath(new URL('project/package.json', fixture.root))); 71 + }); 72 + 73 + it('returns null when invoked inside @bomb.sh/tools itself', async () => { 74 + const fixture = await createFixture({ 75 + 'package.json': '{ "name": "@bomb.sh/tools" }', 76 + }); 77 + createMocks({ env: { INIT_CWD: fileURLToPath(fixture.root) } }); 78 + 79 + expect(await findParentPackage()).toBe(null); 80 + }); 81 + });
+24 -12
src/commands/sync.ts
··· 1 1 import { readlink, rm, symlink } from 'node:fs/promises'; 2 2 import { findPackageJSON } from 'node:module'; 3 3 import { dirname, isAbsolute, relative, resolve } from 'node:path'; 4 - import { platform } from 'node:process'; 4 + import { cwd, env, platform } from 'node:process'; 5 5 import { fileURLToPath, pathToFileURL } from 'node:url'; 6 6 import { NodeHfs } from '@humanfs/node'; 7 7 import { parse } from 'ultramatter'; ··· 15 15 const GITIGNORE_END = '# /bsh:skills'; 16 16 17 17 export async function sync(_ctx: CommandContext): Promise<void> { 18 - const parentPkg = findParentPackage(); 18 + const parentPkg = await findParentPackage(); 19 19 if (!parentPkg) { 20 20 console.info('Skipping sync — no parent project found (running inside @bomb.sh/tools?)'); 21 21 return; ··· 174 174 return { name, description }; 175 175 } 176 176 177 - function findParentPackage(): string | null { 178 - const ownPkg = findPackageJSON(import.meta.url); 179 - if (!ownPkg) return null; 177 + /** 178 + * Locate the consuming project's package.json. The project root must come 179 + * from where the command was invoked, never from this package's physical 180 + * location: under pnpm's isolated layout, import.meta.url resolves through 181 + * the node_modules symlink into node_modules/.pnpm/<hash>/, and walking up 182 + * from there lands in the store, not the user's project. INIT_CWD (set by 183 + * pnpm/npm to the directory the script was run from) is preferred because 184 + * package scripts may rewrite cwd. Returns null when no project is found or 185 + * when invoked inside @bomb.sh/tools itself. 186 + */ 187 + export async function findParentPackage(): Promise<string | null> { 188 + const startDir = env.INIT_CWD ?? cwd(); 189 + const candidate = findPackageJSON(pathToFileURL(`${startDir}/`)); 190 + if (!candidate) return null; 180 191 181 - let cursor = dirname(dirname(ownPkg)); 182 - while (cursor !== dirname(cursor)) { 183 - const candidate = findPackageJSON(pathToFileURL(`${cursor}/`)); 184 - if (!candidate) return null; 185 - if (candidate !== ownPkg) return candidate; 186 - cursor = dirname(dirname(candidate)); 192 + const text = await hfs.text(pathToFileURL(candidate)); 193 + if (!text) return null; 194 + try { 195 + const pkg = JSON.parse(text) as { name?: string }; 196 + if (pkg.name === '@bomb.sh/tools') return null; 197 + } catch { 198 + return null; 187 199 } 188 - return null; 200 + return candidate; 189 201 }