Repository navigation
Spawn vp-managed package managers via node <pm>.cjs directly to remove the Windows .cmd/.ps1 shim hazards #1971
Description
Summary
Proposal: invoke vp-managed package managers (and npm/npx) by spawning the real JS entry with the managed Node directly (node <pm>.cjs <args>), instead of resolving to the Windows .cmd shim and then routing through PowerShell. This removes the root cause behind both the Ctrl+C terminal corruption and the CI stdin hang in every stdio configuration, and drops the PowerShell dependency.
Background
- fix: route Windows
.cmdshims through PowerShell to fix Ctrl+C terminal corruption #1414 routed.cmdshims through PowerShell to fix thecmd.exe"Terminate batch job (Y/N)?" Ctrl+C corruption. - That regressed CI: on Windows runners stdin is not a TTY, and the npm/pnpm/yarn
.ps1wrappers introspect stdin ($MyInvocation.ExpectingInput) and block forever on an open, empty stdin pipe (Ctrl+C breaks the terminal on Windows for commands delegated through package managers #1489, Pressing Ctrl+C after vp run dev, pnpm run dev, or npm run dev leaves the terminal in a broken state on Windows #1176, downstream Bundling on Windows runners with Vite+ will be stuck for several hours until canceled by GitHub tauri-apps/tauri-action#1308 and Using on Windows runners will be stuck for several hours until canceled by GitHub setup-vp#90). Jobs hung ~6h until GitHub cancelled them. - Current fix in
crates/vite_command/src/ps1_shim.rs: skip the PowerShell rewrite whenstdin.is_terminal()is false. It is sound and well-tested.
Why the current approach has structural limits
- It couples two unrelated concerns through a heuristic: terminal Ctrl+C corruption is a console property, but it is gated on stdin TTY-ness.
- It keeps a hard dependency on PowerShell being present plus
-ExecutionPolicy Bypassfor the interactive path. - It still leaves the
cmd.execorruption in place for non-TTY-but-console cases (e.g.vp install < input.txtfrom a real terminal).
Proposal: bypass the shims for vp-managed binaries
vp already has everything needed to skip the shim layer for the binaries it manages:
- The installer writes the real JS entry next to the shims (
crates/vite_install/src/package_manager.rs,crates/vite_install/src/shim.rs):<pm>.cjs,<pm>.cmd, and<pm>.ps1all just runnode <pm>.cjs, e.g.$VP_HOME/package_manager/pnpm/<ver>/pnpm/bin/pnpm.cjs. - vp ships its own Node:
$VP_HOME/js_runtime/node/<ver>/node(.exe).
So for the managed scope, spawn:
$VP_HOME/js_runtime/node/<ver>/node(.exe) .../pnpm/bin/pnpm.cjs <args>
This is byte-for-byte what the shims do internally, minus the wrapper.
Benefits:
- No
cmd.exe, so no Ctrl+C "Terminate batch job (Y/N)?" corruption, in every stdio config. - No
.ps1, so no$MyInvocation.ExpectingInputstdin hang, in every stdio config. - No PowerShell dependency or execution-policy concerns.
- Removes the TTY heuristic for this path; identical behavior in CI and interactive. vp keeps full control of stdio inheritance and signal handling (
execute_with_terminal_guard,fix_stdio_streams).
Scope and fallback
Apply this only to what vp owns (the $VP_HOME managed shims: package managers plus npm/npx under js_runtime), where the .cjs target is known. For arbitrary third-party node_modules/.bin/*.cmd, parsing the shim to find the node target is more involved, so the existing TTY-gated PowerShell rewrite in ps1_shim.rs can stay as the fallback there. Net result: the highest-value path (the package-manager invocation that caused the CI hang) becomes bulletproof, and the general case keeps today's behavior.
Implementation sketch
- In
crates/vite_command/src/lib.rsresolve_program(or a layer above it): when the resolved bin is a vp-managed PM / npm / npx shim, resolve to the sibling<name>.cjsplus the managednode, and return(node_path, [cjs_path, ...args])instead of the.cmd/ PowerShell rewrite. - Keep
ps1_shim::rewrite_cmd_to_powershellfor thenode_modules/.bin/*fallback.
Notes
- setup-vp also ships a defensive mitigation (close child stdin so the
.ps1wrappers see EOF) for users who pin vp < 0.1.21: fix: close stdin on vp invocations to avoid Windows runner hang setup-vp#91.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
fengmk2 commentedon Jun 28, 2026
Update: root cause pinpointed and reproduced
Deeper investigation (and an end-to-end repro) narrowed this down precisely. There are two
.cmd->.ps1routing implementations, and only one is gated:crates/vite_command/src/ps1_shim.rsis_stdin_terminal()crates/vite_task_plan/src/ps1_shim.rs->rewrite_cmd_shim_with_argsvp run <script>(e.g.vp run tauri build) executes through vite-task's plan layer, which rewritestauri.cmd->powershell -File tauri.ps1with only structural checks (inside-workspacenode_modules/.bin+.ps1sibling). No TTY/CI gate. The.ps1wrapper's$input | & node ...finishes the build but then blocks forever draining a non-TTY stdin pipe, so the process never exits and holds the runner's stdio pipe open. vp's gatedps1_shimnever runs for this path, which is whyvp 0.2.1still hung.Reproduced + verified end-to-end in a real Tauri build (tauri-apps/tauri-action#1308): closing the child's stdin made the build proceed past
Finished N bundlesinstead of hanging, confirming the.ps1-reads-stdin mechanism.Near-term fix (in progress): gate the vite-task plan rewrite so it is skipped in non-interactive/CI environments. Branches:
voidzero-dev/vite-task@fix/ps1-stdin-tty-gate+ a vite-plus rev bump.This issue's proposal stays the right long-term fix: invoking
node <pm>.cjsdirectly removes the.cmd/.ps1shim layer entirely, eliminating both the Ctrl+C corruption and the stdin-hang class for vp-managed binaries, instead of gating each routing site.fengmk2 commentedon Jun 28, 2026
fix(plan): skip cmd->ps1 rewrite when stdin is not a terminal (#491)
chore(deps): bump vite-task to pick up the Windows ps1 stdin gate (#1973