Skip to content

fix(exe): route pn/pnpx/pnx through .exe hardlinks on Windows (#11486)#11501

Merged
zkochan merged 4 commits into
mainfrom
fix/11486
May 6, 2026
Merged

fix(exe): route pn/pnpx/pnx through .exe hardlinks on Windows (#11486)#11501
zkochan merged 4 commits into
mainfrom
fix/11486

Conversation

@zkochan

@zkochan zkochan commented May 6, 2026

Copy link
Copy Markdown
Member

Summary

  • Fixes #11486: pn / pnpx / pnx printed the cmd.exe banner and dropped into an interactive command prompt instead of running pnpm when invoked from Git Bash on Windows after a pnpm self-update (or fresh npm install -g @pnpm/exe).
  • Roots out the cmd.exe hop entirely by hardlinking pn.exe / pnpx.exe / pnx.exe to the SEA pnpm.exe and pointing bin at those .exe files. The SEA detects which name it was launched as via process.execPath and prepends dlx for pnpx / pnx.

Why this was broken

@pnpm/exe's install (both setup.js preinstall and linkExePlatformBinary in pnpm self-update) rewrote bin to point those aliases at .cmd wrappers. @zkochan/cmd-shim reads each bin source and infers an interpreter; for .cmd it emits a Bash shim of the form:

exec cmd /C \"\$basedir/.../pnpx.cmd\" \"\$@\"

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

  • New Windows-only tests in `pnpm/artifacts/exe/test/setup.test.ts` and `engine/pm/commands/test/self-updater/selfUpdate.test.ts` repro the chain end-to-end via cmd-shim and Bash, and assert no cmd.exe banner appears (skipped on non-Windows). Committed before the fix so the diff makes the bug observable.
  • Existing tests pass on macOS (`pnpm --filter @pnpm/exe test`, linkExePlatformBinary describe).
  • Lint clean.
  • Windows CI on this PR exercises the new tests.

Written by an agent (Claude Code, claude-opus-4-7).

Summary by CodeRabbit

  • Bug Fixes

    • Command aliases (pn, pnpx, pnx) on Windows now behave correctly in Git Bash/MSYS2, avoiding interactive cmd prompts.
    • pnpx and pnx invocation issues on Windows (dlx execution) have been resolved.
  • Tests

    • Added Windows-specific regression tests validating alias creation and packaging behavior.
  • Chores

    • Packaging and build/setup adjusted so Windows command aliases are delivered as native .exe aliases; added related test/build scripts.

zkochan added 2 commits May 6, 2026 21:31
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.
Copilot AI review requested due to automatic review settings May 6, 2026 19:32
@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Windows 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.

Changes

Windows Alias Support Fix

Layer / File(s) Summary
Changeset & Documentation
.changeset/exe-windows-aliases.md
Adds patch entries for "@pnpm/engine.pm.commands" and "pnpm" and documents the Windows alias fix replacing .cmd wrappers with .exe hardlinks and runtime name detection.
Setup & Packaging
pnpm/artifacts/exe/package.json, pnpm/artifacts/exe/setup.js
Adds devDependency @zkochan/cmd-shim; setup.js now creates pn.exe, pnpx.exe, pnx.exe hardlinks to the SEA platform binary and rewrites package.json bin entries to .exe names.
Self-Updater Wiring
engine/pm/commands/src/self-updater/installPnpm.ts
Windows branch of linkExePlatformBinary updated to create hardlinks for pn, pnpx, pnx to the SEA binary and to update exe package bin mappings to .exe variants during self-update.
CLI Entrypoint
pnpm/src/pnpm.ts
Adds buildArgv() which inspects process.execPath basename to detect alias invocations and conditionally prepends "dlx" so pnpx/pnx invocations behave as dlx.
Tests / Sandbox
pnpm/artifacts/exe/test/setup.test.ts, engine/pm/commands/test/self-updater/selfUpdate.test.ts
Adds Windows-specific sandbox helper and tests verifying bin rewriting to .exe, creation of hardlinks for pn/pnpx/pnx, inode equality with pnpm.exe, and Bash shim behavior using cmd-shim.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰
Hop, hop — hardlinks side by side,
No .cmd detours to bide,
SEA knows the name it meets,
dlx is added, flow completes,
Windows Bash sings — smooth and spry.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.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 and concisely summarizes the main change: fixing pn/pnpx/pnx routing through .exe hardlinks on Windows, directly addressing issue #11486.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from #11486: creates .exe hardlinks for pn/pnpx/pnx aliases, updates bin entries to use .exe variants, implements SEA basename detection, and adds comprehensive Windows-specific tests.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the Windows alias issue. Minor additions to package.json scripts (build-artifacts, test) support the testing infrastructure and are reasonable supporting changes.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/11486

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/exe bin entries for pn/pnpx/pnx to point at .exe files and hardlink those aliases to pnpm.exe (both in setup.js and pnpm self-update logic).
  • Update the SEA pnpm entrypoint to detect pnpx/pnx via process.execPath basename and prepend dlx to argv.
  • Add Windows-only regression tests reproducing the cmd-shim + Bash chain that previously triggered the cmd.exe banner.

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.

Comment thread pnpm/artifacts/exe/test/setup.test.ts Outdated
Comment thread pnpm/artifacts/exe/test/setup.test.ts
zkochan added 2 commits May 6, 2026 21:41
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.
Copilot AI review requested due to automatic review settings May 6, 2026 20:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

@@ -0,0 +1,6 @@
---
"@pnpm/engine.pm.commands": patch
@zkochan zkochan merged commit 997a8ca into main May 6, 2026
17 checks passed
@zkochan zkochan deleted the fix/11486 branch May 6, 2026 20:45
zkochan added a commit that referenced this pull request May 6, 2026
#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.
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.

v11, pnx and pnpx fails

2 participants