Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions packages/scip-ingest/src/runners/codehub-bin-path.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
29 changes: 28 additions & 1 deletion packages/scip-ingest/src/runners/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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=<tool>` (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[],
Expand All @@ -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,
});
Expand Down
Loading