Skip to content

Commit 968fb69

Browse files
ankur-archclaude
andcommitted
fix(cli): canonicalize Windows PATH env key so local builds find bun
On Windows the PATH variable is typically keyed as `Path` (its registry casing). The compute SDK spreads process.env into a plain, case-sensitive object when it prepends node_modules/.bin directories for a local build, so `baseEnv.PATH` reads undefined: the rebuilt PATH drops every inherited entry and is written as a second, truncated `PATH` key next to `Path`. In the spawned shell's case-insensitive environment block the truncated key wins, and `bun run build` fails with "'bun' is not recognized as an internal or external command" even though Bun is installed and on Path. Collapse case-variants of PATH into the canonical `PATH` key at CLI startup, before any command delegates to the SDK. Windows resolves environment variables case-insensitively, so the rename is invisible to this process and its children. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e9b491a commit 968fb69

3 files changed

Lines changed: 121 additions & 0 deletions

File tree

packages/cli/src/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
writeJsonError,
2424
writeJsonSuccess,
2525
} from "./shell/output";
26+
import { canonicalizeWindowsPathKey } from "./shell/path-env";
2627
import { disposePromptState } from "./shell/prompt";
2728
import {
2829
type CliRuntime,
@@ -37,6 +38,11 @@ export interface RunCliOptions extends Partial<CliRuntime> {
3738
}
3839

3940
export async function runCli(options: RunCliOptions = {}): Promise<number> {
41+
// The compute SDK spreads process.env into plain objects when building the
42+
// environment for local build subprocesses; a Windows `Path` key breaks
43+
// that (see canonicalizeWindowsPathKey), so normalize before any command.
44+
canonicalizeWindowsPathKey(process.env);
45+
4046
const runtime = resolveRuntime(options);
4147
const program = createProgram(runtime);
4248
process.exitCode = 0;

packages/cli/src/shell/path-env.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Collapses case-variant spellings of the `PATH` environment variable into
3+
* the canonical `PATH` key, in place. No-op outside Windows.
4+
*
5+
* Windows typically keys the variable as `Path` (its registry casing), and
6+
* `process.env` papers over that with case-insensitive lookups. Anything that
7+
* spreads `process.env` into a plain object — the compute SDK does this when
8+
* it prepends `node_modules/.bin` directories for a local build — loses that
9+
* case-insensitivity: reading `env.PATH` returns undefined, so the rewritten
10+
* PATH drops every inherited entry, and writing `env.PATH` forks a second key
11+
* alongside `Path`. The child's environment block is case-insensitive again,
12+
* so the truncated `PATH` clobbers the real `Path` and the spawned build
13+
* cannot resolve commands like `bun` ("'bun' is not recognized ...").
14+
*
15+
* Windows itself resolves environment variables case-insensitively, so
16+
* renaming the key to `PATH` is invisible to this process and its children.
17+
*/
18+
export function canonicalizeWindowsPathKey(
19+
env: NodeJS.ProcessEnv,
20+
platform: NodeJS.Platform = process.platform,
21+
): void {
22+
if (platform !== "win32") {
23+
return;
24+
}
25+
26+
const variants = Object.keys(env).filter(
27+
(key) => key.toUpperCase() === "PATH" && key !== "PATH",
28+
);
29+
if (variants.length === 0) {
30+
return;
31+
}
32+
33+
// On the real Windows process.env, `env.PATH` already resolves the `Path`
34+
// entry case-insensitively; a plain-object copy needs the variant lookup.
35+
const value = env.PATH ?? env[variants[0]];
36+
for (const variant of variants) {
37+
delete env[variant];
38+
}
39+
// On process.env the deletes above also remove the case-insensitive `PATH`
40+
// entry itself, so always write the canonical key back.
41+
if (value !== undefined) {
42+
env.PATH = value;
43+
}
44+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { canonicalizeWindowsPathKey } from "../src/shell/path-env";
4+
5+
describe("canonicalizeWindowsPathKey", () => {
6+
it("renames the Windows registry-cased Path key to PATH", () => {
7+
const env: NodeJS.ProcessEnv = {
8+
Path: "C:\\Windows\\System32;C:\\Users\\flo\\.bun\\bin",
9+
ComSpec: "C:\\Windows\\System32\\cmd.exe",
10+
};
11+
12+
canonicalizeWindowsPathKey(env, "win32");
13+
14+
expect(Object.keys(env).sort()).toEqual(["ComSpec", "PATH"]);
15+
expect(env.PATH).toBe("C:\\Windows\\System32;C:\\Users\\flo\\.bun\\bin");
16+
});
17+
18+
it("keeps a spread of the env readable and rewritable via env.PATH", () => {
19+
// The failure mode from the field: the compute SDK spreads process.env
20+
// into a plain object and rebuilds PATH from `baseEnv.PATH`. With the
21+
// Windows `Path` casing that read is undefined, so the rebuilt PATH loses
22+
// every inherited entry and the spawned `bun run build` cannot find bun.
23+
const env: NodeJS.ProcessEnv = { Path: "C:\\Users\\flo\\.bun\\bin" };
24+
25+
canonicalizeWindowsPathKey(env, "win32");
26+
const spread = { ...env };
27+
28+
expect(spread.PATH).toBe("C:\\Users\\flo\\.bun\\bin");
29+
expect(
30+
Object.keys(spread).filter((key) => key.toUpperCase() === "PATH"),
31+
).toEqual(["PATH"]);
32+
});
33+
34+
it("collapses multiple case variants without dropping the value", () => {
35+
const env: NodeJS.ProcessEnv = {
36+
path: "ignored",
37+
PATH: "C:\\kept",
38+
};
39+
40+
canonicalizeWindowsPathKey(env, "win32");
41+
42+
expect(env).toEqual({ PATH: "C:\\kept" });
43+
});
44+
45+
it("leaves a canonical PATH untouched", () => {
46+
const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows\\System32" };
47+
48+
canonicalizeWindowsPathKey(env, "win32");
49+
50+
expect(env).toEqual({ PATH: "C:\\Windows\\System32" });
51+
});
52+
53+
it("does nothing when no path variable exists", () => {
54+
const env: NodeJS.ProcessEnv = { HOME: "C:\\Users\\flo" };
55+
56+
canonicalizeWindowsPathKey(env, "win32");
57+
58+
expect(env).toEqual({ HOME: "C:\\Users\\flo" });
59+
});
60+
61+
it("is a no-op outside Windows, where casing is significant", () => {
62+
const env: NodeJS.ProcessEnv = {
63+
Path: "/opt/custom",
64+
PATH: "/usr/bin:/bin",
65+
};
66+
67+
canonicalizeWindowsPathKey(env, "linux");
68+
69+
expect(env).toEqual({ Path: "/opt/custom", PATH: "/usr/bin:/bin" });
70+
});
71+
});

0 commit comments

Comments
 (0)