Skip to content

Commit 08e749c

Browse files
authored
fix: resolve the sync target from the invocation directory, not the package location (#46)
1 parent b19d89a commit 08e749c

3 files changed

Lines changed: 56 additions & 15 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
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

src/commands/sync.test.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { lstat, readlink } from 'node:fs/promises';
22
import { fileURLToPath } from 'node:url';
33
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';
66

77
describe('copySkills', () => {
88
it('symlinks each skill into the destination', async () => {
@@ -54,3 +54,28 @@ describe('copySkills', () => {
5454
expect(await fixture.text('project/skills/build/SKILL.md')).toContain('name: build');
5555
});
5656
});
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+
});

src/commands/sync.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { readlink, rm, symlink } from 'node:fs/promises';
22
import { findPackageJSON } from 'node:module';
33
import { dirname, isAbsolute, relative, resolve } from 'node:path';
4-
import { platform } from 'node:process';
4+
import { cwd, env, platform } from 'node:process';
55
import { fileURLToPath, pathToFileURL } from 'node:url';
66
import { NodeHfs } from '@humanfs/node';
77
import { parse } from 'ultramatter';
@@ -15,7 +15,7 @@ const GITIGNORE_START = '# bsh:skills';
1515
const GITIGNORE_END = '# /bsh:skills';
1616

1717
export async function sync(_ctx: CommandContext): Promise<void> {
18-
const parentPkg = findParentPackage();
18+
const parentPkg = await findParentPackage();
1919
if (!parentPkg) {
2020
console.info('Skipping sync — no parent project found (running inside @bomb.sh/tools?)');
2121
return;
@@ -174,16 +174,28 @@ function parseFrontmatter(content: string): SkillInfo | undefined {
174174
return { name, description };
175175
}
176176

177-
function findParentPackage(): string | null {
178-
const ownPkg = findPackageJSON(import.meta.url);
179-
if (!ownPkg) return null;
180-
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));
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;
191+
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;
187199
}
188-
return null;
200+
return candidate;
189201
}

0 commit comments

Comments
 (0)