diff --git a/scripts/resolvers/index.ts b/scripts/resolvers/index.ts index 60d960112..37a53f5ab 100644 --- a/scripts/resolvers/index.ts +++ b/scripts/resolvers/index.ts @@ -143,6 +143,7 @@ export const RESOLVERS: Record = { // Literal bin dir for skills that skip the shared preamble, where // $GSTACK_BIN is never set (#2906): the per-install root when rendering for // one, else the host's default global root. + INSTALLED_ROOT: (ctx) => ctx.installRoot ? ctx.installRoot.replace(/\/+$/, '') : `~/${getHostConfig(ctx.host).globalRoot}`, INSTALLED_BIN_DIR: (ctx) => ctx.installRoot ? `${ctx.installRoot.replace(/\/+$/, '')}/bin` : `~/${getHostConfig(ctx.host).globalRoot}/bin`, FOREGROUND_DISPATCH_NOTE: () => FOREGROUND_DISPATCH_NOTE, GBRAIN_CONTEXT_LOAD: generateGBrainContextLoad, diff --git a/skillify/SKILL.md.tmpl b/skillify/SKILL.md.tmpl index 50f317d7b..7f10f4c9c 100644 --- a/skillify/SKILL.md.tmpl +++ b/skillify/SKILL.md.tmpl @@ -220,7 +220,7 @@ Resolve the gstack install dir. Two reliable signals (in order): `$B skill list` (the `bundled` row). The skill dir is `/browser-skills/hackernews-frontpage/`, so the install dir is two `dirname` calls above its `_lib/browse-client.ts`. -2. The active gstack skills install at `~/.claude/skills/gstack/`. Read +2. The active gstack skills install at `{{INSTALLED_ROOT}}/`. Read the symlink target if it's a symlink, otherwise use the path directly. Example (run as Bun, not bash, to avoid shell-redirect parsing issues): @@ -232,7 +232,7 @@ import * as path from 'path'; function resolveSdkPath(): string { const candidates = [ - path.join(os.homedir(), '.claude', 'skills', 'gstack', 'browse', 'src', 'browse-client.ts'), + '{{INSTALLED_ROOT}}/browse/src/browse-client.ts'.replace(/^~(?=\/)/, os.homedir()), // Add other install-dir candidates if your environment differs. ]; for (const c of candidates) { diff --git a/test/skillify-host-paths.test.ts b/test/skillify-host-paths.test.ts new file mode 100644 index 000000000..ed7ad5170 --- /dev/null +++ b/test/skillify-host-paths.test.ts @@ -0,0 +1,28 @@ +/** + * /skillify resolved the browse SDK at ~/.claude/skills/gstack on every host, + * so on Codex and the other external hosts it read another host's install (or + * nothing). The install root now comes from the host config (INSTALLED_ROOT). + */ +import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { ALL_HOST_CONFIGS } from '../hosts'; +import { runGeneration } from '../scripts/gen-skill-docs'; + +const out = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-skillify-paths-')); +beforeAll(async () => { expect((await runGeneration({ host: 'all', outputRoot: out })).exitCode).toBe(0); }); +afterAll(() => fs.rmSync(out, { recursive: true, force: true })); + +describe('skillify resolves the browse SDK from its own host install', () => { + for (const host of ALL_HOST_CONFIGS) { + test(host.name, () => { + const file = host.name === 'claude' ? path.join(out, 'skillify/SKILL.md') : path.join(out, host.hostSubdir, 'skills/gstack-skillify/SKILL.md'); + if (!fs.existsSync(file)) return; + const text = fs.readFileSync(file, 'utf8'); + const sdk = text.split('\n').filter(l => l.includes('browse-client.ts') && /homedir|replace/.test(l)); + expect(sdk).toEqual([` '~/${host.globalRoot}/browse/src/browse-client.ts'.replace(/^~(?=\\/)/, os.homedir()),`]); + expect(text).toContain(`The active gstack skills install at \`~/${host.globalRoot}/\``); + }); + } +});