Conversation
Captures the user-reported failure on a fresh Windows CI: when the @pnpm/exe install rewrites bin entries to point at .cmd files, @zkochan/cmd-shim's Bash shim does `exec cmd /C ...target.cmd`, MSYS2 mangles the lone `/C` into a Windows path, and cmd.exe falls into interactive mode (printing its banner instead of running the alias). These tests will fail on `windows-latest` until the follow-up commit points the bin entries at .exe hardlinks of the SEA binary.
The @pnpm/exe install rewrote bin to point pn/pnpx/pnx at .cmd files, which cmd-shim wraps as `exec cmd /C ...target.cmd "$@"` in its Bash shim. MSYS2 / Git Bash mangles the lone `/C` into a Windows path before cmd.exe sees it, so cmd.exe finds no /C or /K and falls into interactive mode — the user sees its banner instead of `pnpm dlx`. Hardlink pn.exe / pnpx.exe / pnx.exe to the SEA pnpm.exe (in setup.js preinstall and in self-update's linkExePlatformBinary) and rewrite those bin entries to the .exe names. cmd-shim emits a direct exec for .exe sources, taking cmd.exe out of the chain entirely. The SEA reads process.execPath's basename and prepends `dlx` when launched as pnpx / pnx.
📝 WalkthroughWalkthroughWindows alias handling for pnpm was changed: .cmd wrapper mappings for pn, pnpx, and pnx were replaced with .exe hardlinks to the SEA binary, the self-update and packaging flows now emit .exe bin entries, and the CLI normalizes argv (prepends "dlx" for pnpx/pnx) so alias invocations behave correctly. Tests and a changeset were added. ChangesWindows Alias Support Fix
Sequence Diagram(s)sequenceDiagram
actor User
participant Shell
participant AliasExe as "pn / pnpx / pnx (.exe)"
participant SEA as "SEA binary (pnpm.exe)"
participant CLI as "pnpm CLI (node)"
User->>Shell: run "pnx <args>"
Shell->>AliasExe: invoke pnx.exe
AliasExe->>SEA: hardlinked exec (same executable)
SEA->>CLI: runtime detects execPath basename -> buildArgv()
CLI->>CLI: prepends "dlx" for pnpx/pnx, runs requested command
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
Fixes a Windows + Git Bash/MSYS2 regression where pn / pnpx / pnx could route through .cmd wrappers and accidentally drop into an interactive cmd.exe session (printing the banner), especially after pnpm self-update or fresh @pnpm/exe installs. The change removes the cmd.exe hop by routing aliases through .exe hardlinks and teaching the SEA entrypoint to translate pnpx/pnx into pnpm dlx.
Changes:
- On Windows, rewrite
@pnpm/exebinentries forpn/pnpx/pnxto point at.exefiles and hardlink those aliases topnpm.exe(both insetup.jsandpnpm self-updatelogic). - Update the SEA
pnpmentrypoint to detectpnpx/pnxviaprocess.execPathbasename and prependdlxto argv. - Add Windows-only regression tests reproducing the cmd-shim + Bash chain that previously triggered the
cmd.exebanner.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
pnpm/src/pnpm.ts |
Detect SEA invocation as pnpx/pnx and prepend dlx to arguments. |
pnpm/artifacts/exe/test/setup.test.ts |
Adds Windows-only regression tests for @pnpm/exe setup + cmd-shim behavior under Bash. |
pnpm/artifacts/exe/setup.js |
On Windows, hardlinks pn.exe/pnpx.exe/pnx.exe to pnpm.exe and rewrites bin to point at .exe entries. |
pnpm/artifacts/exe/package.json |
Adds @zkochan/cmd-shim as a dev dependency for tests. |
pnpm-lock.yaml |
Locks the added dev dependency for the @pnpm/exe importer. |
engine/pm/commands/test/self-updater/selfUpdate.test.ts |
Adds Windows regression coverage for linkExePlatformBinary() hardlinking + bin rewrite. |
engine/pm/commands/src/self-updater/installPnpm.ts |
Updates linkExePlatformBinary() to hardlink alias .exe files and rewrite bin entries on Windows. |
.changeset/exe-windows-aliases.md |
Documents the Windows Git Bash alias fix as a patch release. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two follow-ups from Copilot review on #11501: * Use `'junction'` instead of `'dir'` for the detect-libc symlink on Windows. Non-junction directory symlinks need Developer Mode or admin, which the existing failure-path tests already skip on Windows for; junctions don't. * Probe \`bash --version\` before running the Git Bash / MSYS2 alias test, and skip cleanly if it isn't on PATH (local Windows dev machines often lack it; CI windows-latest ships it). Fold the status check into the assertion so a non-zero exit surfaces in the diff.
The setup.test.ts in this package wasn't running in CI — `@pnpm/exe` had no `.test` script, so `pn -r .test` (what `test-pkgs-all` runs) silently skipped it. The existing tests there have apparently been dead since they were added; the Windows alias repro added in 1e93a1d inherited the same gap. Add `.test` (jest invocation, matching every other workspace package's shape) and a `test` alias so it's picked up by the recursive runner. meta-updater's @pnpm/exe / artifacts branch short-circuits before adding test scripts; preserve that behavior by hand-writing them rather than restructuring the rule.
| @@ -0,0 +1,6 @@ | |||
| --- | |||
| "@pnpm/engine.pm.commands": patch | |||
#11501) * test(exe): add Windows-only repro for #11486 (pn/pnpx/pnx aliases) Captures the user-reported failure on a fresh Windows CI: when the @pnpm/exe install rewrites bin entries to point at .cmd files, @zkochan/cmd-shim's Bash shim does `exec cmd /C ...target.cmd`, MSYS2 mangles the lone `/C` into a Windows path, and cmd.exe falls into interactive mode (printing its banner instead of running the alias). These tests will fail on `windows-latest` until the follow-up commit points the bin entries at .exe hardlinks of the SEA binary. * fix(exe): route pn/pnpx/pnx through .exe hardlinks on Windows (#11486) The @pnpm/exe install rewrote bin to point pn/pnpx/pnx at .cmd files, which cmd-shim wraps as `exec cmd /C ...target.cmd "$@"` in its Bash shim. MSYS2 / Git Bash mangles the lone `/C` into a Windows path before cmd.exe sees it, so cmd.exe finds no /C or /K and falls into interactive mode — the user sees its banner instead of `pnpm dlx`. Hardlink pn.exe / pnpx.exe / pnx.exe to the SEA pnpm.exe (in setup.js preinstall and in self-update's linkExePlatformBinary) and rewrite those bin entries to the .exe names. cmd-shim emits a direct exec for .exe sources, taking cmd.exe out of the chain entirely. The SEA reads process.execPath's basename and prepends `dlx` when launched as pnpx / pnx. * test(exe): make Windows alias tests robust to local-dev environments Two follow-ups from Copilot review on #11501: * Use `'junction'` instead of `'dir'` for the detect-libc symlink on Windows. Non-junction directory symlinks need Developer Mode or admin, which the existing failure-path tests already skip on Windows for; junctions don't. * Probe \`bash --version\` before running the Git Bash / MSYS2 alias test, and skip cleanly if it isn't on PATH (local Windows dev machines often lack it; CI windows-latest ships it). Fold the status check into the assertion so a non-zero exit surfaces in the diff. * test(exe): wire @pnpm/exe into the recursive test runner The setup.test.ts in this package wasn't running in CI — `@pnpm/exe` had no `.test` script, so `pn -r .test` (what `test-pkgs-all` runs) silently skipped it. The existing tests there have apparently been dead since they were added; the Windows alias repro added in 1e93a1d inherited the same gap. Add `.test` (jest invocation, matching every other workspace package's shape) and a `test` alias so it's picked up by the recursive runner. meta-updater's @pnpm/exe / artifacts branch short-circuits before adding test scripts; preserve that behavior by hand-writing them rather than restructuring the rule.
Summary
pn/pnpx/pnxprinted the cmd.exe banner and dropped into an interactive command prompt instead of running pnpm when invoked from Git Bash on Windows after apnpm self-update(or freshnpm install -g @pnpm/exe).pn.exe/pnpx.exe/pnx.exeto the SEApnpm.exeand pointingbinat those.exefiles. The SEA detects which name it was launched as viaprocess.execPathand prependsdlxforpnpx/pnx.Why this was broken
@pnpm/exe's install (both
setup.jspreinstall andlinkExePlatformBinaryinpnpm self-update) rewrotebinto point those aliases at.cmdwrappers.@zkochan/cmd-shimreads each bin source and infers an interpreter; for.cmdit emits a Bash shim of the form:In MSYS2 / Git Bash on Windows, the runtime mangles the lone `/C` switch into a Windows path before cmd.exe sees it. cmd.exe then finds no `/C` or `/K` switch and falls into interactive mode — the user sees its banner. `pnpm` itself worked because its bin source was already `pnpm.exe`, so cmd-shim emits a direct `exec` with no `cmd.exe` in the chain.
Test plan
Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
Bug Fixes
Tests
Chores