fix(windows): prefer npm.cmd over npm.ps1, skip .py argv0 in relaunch

Two fixes from teknium1's next install run:

1. **npm install: "npm.ps1 cannot be loaded because running scripts is
   disabled on this system."**  Get-Command's default PATHEXT ordering
   picked up ``npm.ps1`` (the PowerShell shim) ahead of ``npm.cmd`` (the
   batch shim).  Most Windows users have PowerShell's execution policy
   set to Restricted or RemoteSigned, which blocks unsigned ``.ps1``
   files.  ``npm.cmd`` has no such restriction and works universally.

   Install-NodeDeps now detects when Get-Command returned npm.ps1, looks
   for a sibling npm.cmd in the same directory, and prefers it.  Prints
   an info line so the user sees why.  Emits a warning + hint if only
   npm.ps1 is available.

2. **"Launch hermes chat now? Y" crashes with "%1 is not a valid Win32
   application" on Windows installs.**  The setup wizard calls
   ``relaunch(["chat"])``; ``resolve_hermes_bin()`` returned
   ``sys.argv[0]`` which was ``...\\hermes_cli\\main.py`` (because hermes
   was launched via ``python -m hermes_cli.main`` during setup).

   On Windows, ``os.access(script.py, os.X_OK)`` returns True because
   PATHEXT lists ``.py`` when the Python launcher is registered — but
   ``subprocess.run([script.py, ...])`` can't actually execute a ``.py``
   directly.  CreateProcessW needs a real PE file.

   Fixed ``resolve_hermes_bin`` to reject ``.py``/``.pyc`` argv0 values
   on Windows specifically.  Falls through to ``shutil.which("hermes")``
   (hermes.exe in the venv Scripts dir) or, as a final fallback, lets
   build_relaunch_argv build ``[sys.executable, "-m", "hermes_cli.main"]``
   which is bulletproof.  POSIX behaviour unchanged — ``.py`` argv0 with
   a shebang + chmod+x is still a valid exec target there.

3 new tests cover the Windows paths: .py argv0 + hermes.exe on PATH →
returns hermes.exe; .py argv0 + no PATH → returns None (caller uses
python -m); POSIX + executable .py → still accepted.

26 relaunch tests pass, no POSIX regressions.
This commit is contained in:
Teknium 2026-05-07 18:29:17 -07:00
parent 21efeb51bb
commit a2efad6bea
3 changed files with 94 additions and 6 deletions

View file

@ -963,9 +963,17 @@ function Install-NodeDeps {
return
}
# Resolve npm.cmd to an absolute path so PATHEXT doesn't bite us when
# the installer runs in a session that hasn't refreshed PATH since the
# Node.js install. Get-Command respects PATHEXT.
# Resolve npm explicitly to npm.cmd, NOT npm.ps1. Node.js on Windows
# ships BOTH npm.cmd (a batch shim) and npm.ps1 (a PowerShell shim).
# Get-Command's default ordering picks whichever comes first in PATHEXT,
# and on many systems that's .ps1 — but .ps1 requires scripts to be
# enabled in PowerShell's execution policy, which most Windows users
# don't have (the Restricted / RemoteSigned default blocks unsigned
# .ps1 files). .cmd has no such restriction and works on every box.
#
# Strategy: look next to the npm shim we found and prefer npm.cmd if
# it exists in the same directory. Fall back to whatever Get-Command
# returned if we can't find a .cmd sibling.
$npmCmd = Get-Command npm -ErrorAction SilentlyContinue
if (-not $npmCmd) {
Write-Warn "npm not found on PATH — skipping Node.js dependencies."
@ -973,6 +981,16 @@ function Install-NodeDeps {
return
}
$npmExe = $npmCmd.Source
if ($npmExe -like "*.ps1") {
$npmCmdSibling = Join-Path (Split-Path $npmExe -Parent) "npm.cmd"
if (Test-Path $npmCmdSibling) {
Write-Info "Using npm.cmd (PowerShell execution policy blocks npm.ps1)"
$npmExe = $npmCmdSibling
} else {
Write-Warn "Only npm.ps1 available — install may fail if script execution is disabled."
Write-Info " If it fails, either enable PS script execution or install Node via winget."
}
}
# Helper: run "npm install" in a given directory and surface the real
# error when it fails. Returns $true on success.