Skip to content

fix(cli): canonicalize Windows PATH env key so local builds can find bun#115

Closed
ankur-arch wants to merge 3 commits into
mainfrom
fix/windows-path-env-casing
Closed

fix(cli): canonicalize Windows PATH env key so local builds can find bun#115
ankur-arch wants to merge 3 commits into
mainfrom
fix/windows-path-env-casing

Conversation

@ankur-arch

Copy link
Copy Markdown

Fixes the Windows-only failure reported in Discord: prisma-cli app build / app deploy on a freshly scaffolded Prisma Compute + Next.js project fails immediately with:

>'bun' is not recognized as an internal or external command,
operable program or batch file.

…even though Bun is installed, on Path, and bun run build succeeds when run manually from the same directory. The same project works via WSL.

How the issue was validated

The reporter's trace points at @prisma/compute-sdk/dist/workspace.js, which is compiled and outside this repo, so the failure was reproduced in a sandboxed mini Windows environment built around the real installed SDK code:

  1. Environment model — a faithful simulation of the three Windows semantics involved: the inherited environment keys its path variable as Path (registry casing); a child's environment block is case-insensitive, so keys differing only in case collide; and cmd.exe resolves bare command names by scanning the effective Path + PATHEXT. The sandbox mirrors the reporter's machine: Bun under %USERPROFILE%\.bun\bin on the system Path, and a scaffolded project with "packageManager": "bun@1.3.14" and "build": "next build".
  2. Repro against the real SDK — calling the SDK's actual buildCommandEnv() with that environment shows it emits an env containing both the original Path (full) and a new PATH holding only node_modules/.bin directories. In the child's case-insensitive block the truncated PATH clobbers Path, and cmd.exe can no longer resolve bun — reproducing the exact reported error. The control case (the user's own shell) resolves bun fine, matching the report that manual builds work.
  3. End-to-end through the real spawn path — running the SDK's actual runBuildCommand() (the failing frame, shell: true) in a process whose environment keys the path variable as Path makes the spawned shell fail with bun: command not found (exit 127 — the POSIX twin of the Windows error). Re-running the identical build after applying this PR's canonicalizeWindowsPathKey() succeeds.

What the issue was

The scaffolded project sets "packageManager": "bun@1.3.14", so the SDK resolves the local build command to the string bun run build and runs it through a shell. Before spawning, it constructs the child environment like this (workspace.js):

const env = await buildCommandEnv(options.appPath, { ...process.env, ...options.env }, ...);
// inside buildCommandEnv:
return { ...baseEnv, PATH: [...binDirs, baseEnv.PATH].filter(Boolean).join(path.delimiter) };

On Windows the real variable is keyed Path, and process.env hides that behind case-insensitive lookups. Spreading process.env into a plain object removes that magic:

  • baseEnv.PATH reads undefined (plain objects are case-sensitive), so the rebuilt PATH silently drops every inherited entry — including ~\.bun\bin;
  • writing PATH forks a second key alongside the untouched Path;
  • the spawned child's environment block is case-insensitive again, so the truncated PATH clobbers the real Path, and cmd.exe cannot resolve bun.

This explains every observation in the report: manual bun run build works (unmodified environment), WSL works (Linux keys the variable literally PATH), every launcher (bunx/npx/node directly) fails identically (the bug is downstream in the build env construction), and --build-type bun fails differently (different code path).

How it was fixed

@prisma/compute-sdk is an external dependency, so the fix lands in the CLI, which owns the process the SDK inherits its environment from. runCli() now calls canonicalizeWindowsPathKey(process.env) (new packages/cli/src/shell/path-env.ts) before dispatching any command: on Windows it collapses all case-variants of the path variable into the single canonical PATH key. After that, the SDK's spread-and-rebuild sees baseEnv.PATH, keeps the full inherited path with node_modules/.bin prepended, and produces no case-colliding duplicate keys.

Windows resolves environment variables case-insensitively, so renaming PathPATH is invisible to the CLI process and everything it spawns; on macOS/Linux the function is a no-op.

Testing: new unit tests in packages/cli/tests/path-env.test.ts cover the registry-cased rename, the spread/rewrite failure mode from the field, multi-variant collapse, and the non-Windows no-op. Full suite: 46 files / 615 tests pass; lint and build clean.

🤖 Generated with Claude Code

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>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • Bug Fixes

    • Improved Windows environment handling by normalizing case-variant PATH keys to ensure the PATH value is preserved and applied consistently when running the CLI.
    • Prevented scenarios where Path/PATH key casing differences could lead to missing binaries during command execution.
  • Tests

    • Added unit and integration coverage to validate Windows behavior, deterministic PATH precedence, and no-op behavior on non-Windows platforms.

Walkthrough

This PR adds canonicalizeWindowsPathKey in packages/cli/src/shell/path-env.ts to normalize case-variant PATH environment keys into PATH on Windows, and no-ops on other platforms. packages/cli/src/cli.ts calls it on process.env at the start of runCli before runtime resolution and command handling. New Vitest tests cover canonicalization behavior, variant collapsing, no-op cases, and an integration flow that verifies a shell subprocess can find a binary after the environment is rebuilt.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main Windows PATH environment key fix and its impact on local builds.
Description check ✅ Passed The description is directly about the same Windows bun-path failure and the CLI fix, so it is relevant.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/windows-path-env-casing
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/windows-path-env-casing

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 9, 2026
Address review feedback on the Windows PATH canonicalization fix:

- Give the multi-variant case a fixed precedence (existing `PATH`, then the
  Windows-native `Path`, then any remaining spelling) so the result no longer
  depends on Object.keys insertion order.
- Trim the helper's top-level comment to the "why"; the detailed spread
  failure mode now lives next to the test that reproduces it.
- Add an integration test that drives the real end-to-end path — process.env
  spread into a plain object, PATH rebuilt with node_modules/.bin prepended
  (the compute SDK's shape), then a spawned shell child that must still
  resolve a binary on the inherited path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ankur-arch

Copy link
Copy Markdown
Author

Pushed a follow-up addressing the review (commit deterministic PATH precedence + integration coverage).

Merge-readiness — global process.env mutation is intentional and safe. I grepped the CLI source: nothing reads the registry-cased Path key or otherwise inspects the original casing — every consumer uses env.PATH or specific PRISMA_*/npm_* keys. Windows resolves env lookups case-insensitively, so the PathPATH rename is invisible to this process. It also helps the one other subprocess we spawn: the update-check worker (shell/update-check.ts) does { ...process.env }, which had the same latent truncation and is now protected. No telemetry path touches it.

Deterministic precedence. When several spellings coexist and there's no canonical PATH, precedence is now fixed — existing PATH, then the Windows-native Path, then any remaining variant — instead of depending on Object.keys insertion order. New test asserts Path wins regardless of ordering.

Comment placement. Trimmed the helper's top-level block to the "why" and moved the detailed spread failure-mode explanation next to the test that reproduces it.

Integration test. Added tests/path-env.integration.test.ts, which drives the real chain the fix protects: process.env → spread into a plain object → PATH rebuilt with node_modules/.bin prepended (the compute SDK's exact env shape) → a spawned shell child that must still resolve a binary placed on the inherited path. On Windows CI it reproduces the original collision (without the fix, the full Path and truncated PATH collide case-insensitively and the binary is unresolved); on POSIX it validates the rewrite preserves inherited entries.

Full suite now 617 tests passing. The two lint errors in controllers/project.ts are pre-existing on main (confirmed in a clean origin/main worktree) and out of scope here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/src/shell/path-env.ts (1)

17-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

selectPreferredVariant's "Path" branch is effectively dead code.

selectPreferredVariant is only reached when both env.PATH and env.Path are undefined (line 36). But variants is derived from Object.keys(env), so if a literal "Path" key were among variants, env.Path would already be defined and the ?? chain would short-circuit before selectPreferredVariant is called. That means variants.includes("Path") inside the helper (line 49) can never be true in practice — it only ever returns variants[0].

A consequence: for the truly order-dependent case — several non-canonical spellings that are not exactly "PATH" or "Path" (e.g. "PaTH", "pATh") — the result does depend on Object.keys insertion order, since it falls through to variants[0]. This contradicts the intent described in the comment on lines 32-35 ("the result never depends on key insertion order").

The test added for this ("prefers the Windows-native Path over other variants, independent of insertion order") doesn't actually exercise this fallback path either — it passes because env.Path is read directly, not because of selectPreferredVariant's tie-break logic.

Consider simplifying selectPreferredVariant to just variants[0] (removing the misleading dead check), or, if genuine multi-variant, non-Path-cased inputs are a real concern, add explicit precedence there.

♻️ Simplification
-/** The Windows-native `Path` if present, otherwise the first variant. */
-function selectPreferredVariant(variants: string[]): string {
-  return variants.includes("Path") ? "Path" : variants[0];
-}
+/** Arbitrary but deterministic pick when neither `PATH` nor `Path` is present. */
+function selectPreferredVariant(variants: string[]): string {
+  return variants[0];
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/shell/path-env.ts` around lines 17 - 50, The dead `"Path"`
check in selectPreferredVariant should be removed or replaced with logic that
actually handles the fallback case reached by canonicalizeWindowsPathKey.
Simplify the helper to match the real behavior (it only receives non-canonical
variants when env.PATH and env.Path are both absent), and if order-independent
precedence is still required for mixed-case keys like PaTH/pATh, make that
selection explicit rather than relying on variants[0]. Update the tests around
canonicalizeWindowsPathKey/selectPreferredVariant so they cover the true
fallback path instead of only the env.Path shortcut.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/cli/src/shell/path-env.ts`:
- Around line 17-50: The dead `"Path"` check in selectPreferredVariant should be
removed or replaced with logic that actually handles the fallback case reached
by canonicalizeWindowsPathKey. Simplify the helper to match the real behavior
(it only receives non-canonical variants when env.PATH and env.Path are both
absent), and if order-independent precedence is still required for mixed-case
keys like PaTH/pATh, make that selection explicit rather than relying on
variants[0]. Update the tests around
canonicalizeWindowsPathKey/selectPreferredVariant so they cover the true
fallback path instead of only the env.Path shortcut.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 71ad0396-f1e8-4906-b48d-7610751308c6

📥 Commits

Reviewing files that changed from the base of the PR and between 968fb69 and 81be1fa.

📒 Files selected for processing (3)
  • packages/cli/src/shell/path-env.ts
  • packages/cli/tests/path-env.integration.test.ts
  • packages/cli/tests/path-env.test.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 9, 2026
kristof-siket
kristof-siket previously approved these changes Jul 9, 2026
Apply review feedback from Kristof and CodeRabbit:

- Remove `selectPreferredVariant`: its `"Path"` branch was dead code, since
  `env.PATH` already resolves `Path` case-insensitively on the real
  process.env and short-circuits before the helper runs. Replace with a sort
  over the remaining variants so the fallback is genuinely independent of key
  insertion order (the earlier claim relied on the dead branch).
- Rework the precedence test to exercise that sorted fallback with exotic
  spellings (`PaTH`), the path CodeRabbit noted the old test never hit.
- Strip comments across the change down to the non-trivial "why".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ankur-arch

Copy link
Copy Markdown
Author

Applied Kristof's and CodeRabbit's feedback in the latest commit:

  • Stripped comments across the change down to only the non-trivial "why" — the redundant call-site block in cli.ts and the verbose test/helper docstrings are gone.
  • CodeRabbit's dead-code finding (selectPreferredVariant): correct. The "Path" branch was unreachable — env.PATH ?? env.Path already resolved Path case-insensitively on the real process.env and short-circuited before the helper ran, so it only ever returned variants[0], which is order-dependent for exotic spellings. Removed the helper and replaced the fallback with a sort over the remaining variants, so the "independent of insertion order" guarantee is now real rather than resting on the dead branch.
  • Reworked the precedence test to actually exercise that sorted fallback with a mixed-case key (PaTH) and no Path/PATH — the path CodeRabbit noted the old test never hit.

Full suite still 617 passing; the SDK sandbox repro still confirms bug-without-fix / resolved-with-fix.

@ankur-arch

Copy link
Copy Markdown
Author

Closing in favor of prisma/project-compute#105, which fixes this at the correct layer.

The root cause lives in the compute SDK's buildCommandEnv (sdk/src/workspace.ts): it spawns the build process, so it owns constructing a valid child env, and it already has platform-aware code that simply handled the Windows Path vs PATH casing wrong. I validated this against the real SDK source in a sandbox — same reproduction as the analysis in this PR.

This PR canonicalized process.env at CLI startup, which did fix the reported symptom, but it's a workaround upstream of the actual defect: it mutates global process.env, and it only protects the CLI while other SDK consumers (e.g. the headless build-runner) stay broken. Once #105 lands, this becomes redundant.

Thanks @kristof-siket for the review, and @AmanVarshney01 for calling out the right layer. Reproduction details and a review left on #105.

@ankur-arch ankur-arch closed this Jul 9, 2026
AmanVarshney01 added a commit that referenced this pull request Jul 9, 2026
Bumps `@prisma/compute-sdk` 0.33.0 -> 0.34.0 to pick up the Windows
build fix
([project-compute#105](prisma/project-compute#105),
published as 0.34.0).

## What this fixes

On Windows, `prisma-cli app build` and `app deploy` failed immediately
with `'bun' is not recognized as an internal or external command` even
with Bun installed. The SDK's `buildCommandEnv` wrote a literal `PATH`
key over Windows' `Path`-cased variable, forking a duplicate key whose
bin-dirs-only value shadowed the real system PATH in the spawned build
shell. 0.34.0 resolves the key case-insensitively and extends it in
place.

Supersedes #115, which patched the same bug CLI-side; the root cause is
SDK-owned.

## Notes

- The lockfile was still resolving compute-sdk 0.31.0 despite the 0.33.0
pin in `packages/cli/package.json`; this refresh brings the installed
version in line with the manifest.
- Verified the published 0.34.0 tarball contains the fix, and the full
CLI suite (45 files, 624 tests) plus build pass against it.
- Windows follow-ups tracked SDK-side: execa migration for the `.cmd`
launcher ladder and a `windows-latest` CI leg.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants