diff --git a/src/launch.test.ts b/src/launch.test.ts index 7be1650..b0c52c3 100644 --- a/src/launch.test.ts +++ b/src/launch.test.ts @@ -222,6 +222,43 @@ describe("discoverSmalltalkDir (fresh-install hook discovery, no SMALLTALK_DIR n }); }); +describe("discoverSmalltalkDir on a PACKAGED layout (repo under /lib/smalltalk)", () => { + const saved = process.env["SMALLTALK_DIR"]; + afterEach(() => { + if (saved === undefined) delete process.env["SMALLTALK_DIR"]; + else process.env["SMALLTALK_DIR"] = saved; + }); + + // Nix (and any packager that installs the repo under lib/) puts `st` at /bin/st while the repo lives + // at /lib/smalltalk — so the bin/st -> grandparent assumption lands one level ABOVE the hooks. + it("finds the hooks under /lib/smalltalk, not just at the candidate root", () => { + const pkg = mkdtempSync(join(tmpdir(), "convoy-sm-pkg-")); + try { + const repo = join(pkg, "lib", "smalltalk"); + mkdirSync(join(repo, "examples", "claude-code", "hooks"), { recursive: true }); + writeFileSync(join(repo, "examples", "claude-code", "hooks", "session-start.sh"), "#!/bin/sh\n"); + process.env["SMALLTALK_DIR"] = pkg; // the package root — hooks are one level down + expect(discoverSmalltalkDir()).toBe(repo); + } finally { + rmSync(pkg, { recursive: true, force: true }); + } + }); + + it("still prefers the candidate root when the hooks are there (checkout layout wins)", () => { + const dir = mkdtempSync(join(tmpdir(), "convoy-sm-both-")); + try { + for (const base of [dir, join(dir, "lib", "smalltalk")]) { + mkdirSync(join(base, "examples", "claude-code", "hooks"), { recursive: true }); + writeFileSync(join(base, "examples", "claude-code", "hooks", "session-start.sh"), "#!/bin/sh\n"); + } + process.env["SMALLTALK_DIR"] = dir; + expect(discoverSmalltalkDir()).toBe(dir); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); + describe("discoverSmalltalkDir via ST_BIN (the off-PATH hooks discovery — Johannes false-negative)", () => { const savedSt = process.env["SMALLTALK_DIR"]; const savedBin = process.env["ST_BIN"]; diff --git a/src/launch.ts b/src/launch.ts index 5616a64..13986c8 100644 --- a/src/launch.ts +++ b/src/launch.ts @@ -86,7 +86,16 @@ export function discoverSmalltalkDir(): string | null { if (fromPath) candidates.push(fromPath); } candidates.push(join(dirname(dirname(fileURLToPath(import.meta.url))), "..", "smalltalk")); - for (const c of candidates) if (hasHooks(c)) return c; + // Each candidate is tried BOTH as the repo root and as `/lib/smalltalk`. A packaged install + // (Nix, and any packager that installs the repo under `lib/`) puts `st` at `/bin/st` while the repo + // itself lives at `/lib/smalltalk` — so the `/bin/st` → grandparent assumption lands on + // ``, one level ABOVE the hooks. Without this the hooks ship in the package but are undiscoverable, + // and `doctor` reports a blocking "smalltalk hooks NOT found" on a correct install. + for (const c of candidates) { + if (hasHooks(c)) return c; + const packaged = join(c, "lib", "smalltalk"); + if (hasHooks(packaged)) return packaged; + } return null; }