From 01f532b52e0d025393a7713d50b045eee30f23fe Mon Sep 17 00:00:00 2001 From: Laith Al-Saadoon <9553966+theagenticguy@users.noreply.github.com> Date: Fri, 29 May 2026 07:26:19 -0500 Subject: [PATCH] fix(scip-ingest): prepend ~/.codehub/bin to indexer spawn PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SCIP indexers installed by `codehub setup --scip=` land in ~/.codehub/bin, but the analyze runner spawned them with the inherited PATH unchanged. An ambient version-manager shim (mise/asdf) that resolves on PATH but has no version pinned for the project exits non-zero before the real indexer runs — the "No version is set for shim" failure — and could shadow a perfectly good setup-installed binary, making the language skip. Prepend ~/.codehub/bin to the spawn env PATH in runCommand (the single chokepoint for both the index spawn and the version probe) so codehub's own installed indexers win over a broken shim. Honors a caller-supplied PATH in envOverlay, is cross-platform (Windows `Path` casing + `;` delimiter), and idempotent. This is the no-vendoring fix for the gap surfaced on the bonk run: indexers stay opt-in installs (ADR 0015 keeps native binaries out of the npm tarball), but once installed they resolve reliably. 73/73 scip-ingest tests pass (7 new covering prepend/idempotency/empty-PATH/ caller-PATH/Windows-casing/no-mutation). --- .../src/runners/codehub-bin-path.test.ts | 60 +++++++++++++++++++ packages/scip-ingest/src/runners/index.ts | 29 ++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 packages/scip-ingest/src/runners/codehub-bin-path.test.ts diff --git a/packages/scip-ingest/src/runners/codehub-bin-path.test.ts b/packages/scip-ingest/src/runners/codehub-bin-path.test.ts new file mode 100644 index 0000000..f0e27e3 --- /dev/null +++ b/packages/scip-ingest/src/runners/codehub-bin-path.test.ts @@ -0,0 +1,60 @@ +/** + * Tests for `withCodehubBinOnPath` — the spawn-env PATH shim that makes + * `codehub setup --scip` installed indexers (under ~/.codehub/bin) win over + * an ambient version-manager shim that resolves on PATH but can't pick a + * version. See `runCommand` in ./index.ts. + * + * The helper reads `homedir()` and `process.platform`, so we compute the + * expected bin dir + delimiter the same way rather than hard-coding a + * platform — these assertions hold on Linux, macOS, and Windows CI legs. + */ + +import assert from "node:assert/strict"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { withCodehubBinOnPath } from "./index.js"; + +const BIN_DIR = join(homedir(), ".codehub", "bin"); +const SEP = process.platform === "win32" ? ";" : ":"; + +test("prepends ~/.codehub/bin ahead of the existing PATH", () => { + const out = withCodehubBinOnPath({ PATH: `/usr/bin${SEP}/bin` }); + assert.equal(out["PATH"], `${BIN_DIR}${SEP}/usr/bin${SEP}/bin`); +}); + +test("is idempotent — does not double-prepend when bin dir is already first", () => { + const already = `${BIN_DIR}${SEP}/usr/bin`; + const out = withCodehubBinOnPath({ PATH: already }); + assert.equal(out["PATH"], already, "PATH should be unchanged when bin dir leads"); +}); + +test("sets PATH to just the bin dir when PATH is empty", () => { + const out = withCodehubBinOnPath({ PATH: "" }); + assert.equal(out["PATH"], BIN_DIR); +}); + +test("sets PATH to just the bin dir when PATH is absent entirely", () => { + const out = withCodehubBinOnPath({}); + assert.equal(out["PATH"], BIN_DIR); +}); + +test("honors a caller-supplied PATH (envOverlay value), not just process.env", () => { + // The runner merges `{ ...process.env, ...envOverlay }` BEFORE calling this + // helper, so a caller PATH override is already the resolved value here. + const out = withCodehubBinOnPath({ PATH: "/caller/supplied" }); + assert.equal(out["PATH"], `${BIN_DIR}${SEP}/caller/supplied`); +}); + +test("preserves other env vars untouched", () => { + const out = withCodehubBinOnPath({ PATH: "/bin", HOME: "/home/x", FOO: "bar" }); + assert.equal(out["HOME"], "/home/x"); + assert.equal(out["FOO"], "bar"); +}); + +test("does not mutate the input env object", () => { + const input = { PATH: "/bin" }; + const out = withCodehubBinOnPath(input); + assert.equal(input["PATH"], "/bin", "input must be left unmodified"); + assert.notEqual(out, input, "should return a new object"); +}); diff --git a/packages/scip-ingest/src/runners/index.ts b/packages/scip-ingest/src/runners/index.ts index 1d8b9eb..0c2998a 100644 --- a/packages/scip-ingest/src/runners/index.ts +++ b/packages/scip-ingest/src/runners/index.ts @@ -923,6 +923,33 @@ type CommandOutcome = | { kind: "failed"; exitCode: number; stdout: string; stderr: string } | { kind: "missing" }; +/** + * Prepend `~/.codehub/bin` to the spawn environment's PATH so SCIP indexers + * installed by `codehub setup --scip=` (clang, ruby, kotlin jar) win + * over an ambient version-manager shim that resolves on PATH but can't pick a + * version (the mise/asdf "No version is set for shim" failure — see + * `detectVersionManagerShimFailure`). Without this, a setup-installed indexer + * could be shadowed by a broken shim earlier on PATH and the language would + * skip even though codehub installed a working binary. + * + * Honors a caller-supplied PATH in `envOverlay` (we read the resolved value + * off `env`, not `process.env`). Cross-platform: matches the PATH key + * case-insensitively (Windows uses `Path`) and uses the platform delimiter. + * Idempotent — never double-prepends. + */ +export function withCodehubBinOnPath(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const binDir = join(homedir(), ".codehub", "bin"); + // Find the PATH key honoring Windows' `Path` casing; default to "PATH". + const pathKey = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH"; + const current = env[pathKey] ?? ""; + const sep = process.platform === "win32" ? ";" : ":"; + const segments = current.split(sep); + // Idempotent: if binDir is already the first segment, leave env untouched. + if (segments[0] === binDir) return env; + const nextPath = current.length > 0 ? `${binDir}${sep}${current}` : binDir; + return { ...env, [pathKey]: nextPath }; +} + function runCommand( cmd: string, args: readonly string[], @@ -940,7 +967,7 @@ function runCommand( // (js/shell-command-*) that this is not a shell invocation. const child = spawn(cmd, args as string[], { cwd, - env: { ...process.env, ...envOverlay }, + env: withCodehubBinOnPath({ ...process.env, ...envOverlay }), stdio: ["ignore", "pipe", "pipe"], shell: false, });