fix(windows): prefer cmd npm shim on PATH fallback

This commit is contained in:
helix4u 2026-06-21 14:06:39 -06:00
parent 8e4d2fd23f
commit 7502d38bf9
2 changed files with 60 additions and 1 deletions

View file

@ -290,13 +290,41 @@ def find_hermes_node_executable(command: str) -> str | None:
return None
def find_node_executable_on_path(command: str) -> str | None:
"""Return a Node/npm executable from PATH with Windows shim ordering.
``shutil.which("npm")`` can resolve an extensionless npm shim before the
``.cmd`` shim on Windows. Python's CreateProcess cannot execute that shim
directly, so prefer the launchable variants explicitly for Hermes-owned
subprocesses.
"""
if sys.platform != "win32":
return shutil.which(command)
command_str = str(command)
has_path_separator = any(
sep and sep in command_str for sep in (os.sep, os.altsep, "/", "\\")
)
if has_path_separator:
return command_str if Path(command_str).is_file() else None
for name in _candidate_node_command_names(command_str):
for directory in os.environ.get("PATH", "").split(os.pathsep):
if not directory:
continue
candidate = Path(directory) / name
if candidate.is_file():
return str(candidate)
return None
def find_node_executable(command: str) -> str | None:
"""Resolve a Node.js command, preferring Hermes-managed installs.
This is for Hermes-owned subprocesses that should not be broken by a bad,
missing, or elevation-triggering system Node/npm on PATH.
"""
return find_hermes_node_executable(command) or shutil.which(command)
return find_hermes_node_executable(command) or find_node_executable_on_path(command)
def with_hermes_node_path(env: dict[str, str] | None = None) -> dict[str, str]:

View file

@ -9,6 +9,8 @@ import hermes_constants
from hermes_constants import (
VALID_REASONING_EFFORTS,
find_hermes_node_executable,
find_node_executable,
find_node_executable_on_path,
get_default_hermes_root,
get_hermes_home,
iter_hermes_node_dirs,
@ -131,6 +133,35 @@ class TestHermesManagedNode:
assert find_hermes_node_executable("npm") == str(npm_cmd)
def test_windows_path_fallback_prefers_npm_cmd(self, tmp_path, monkeypatch):
bin_dir = tmp_path / "nodejs"
bin_dir.mkdir()
extensionless = bin_dir / "npm"
powershell = bin_dir / "npm.ps1"
npm_cmd = bin_dir / "npm.cmd"
extensionless.write_text("#!/usr/bin/env node\n")
powershell.write_text("Write-Output npm\n")
npm_cmd.write_text("@echo off\n")
monkeypatch.setattr(hermes_constants.sys, "platform", "win32")
monkeypatch.setenv("PATH", str(bin_dir))
assert find_node_executable_on_path("npm") == str(npm_cmd)
def test_windows_node_executable_falls_back_to_safe_path_shim(self, tmp_path, monkeypatch):
home = tmp_path / "hermes"
home.mkdir()
bin_dir = tmp_path / "nodejs"
bin_dir.mkdir()
extensionless = bin_dir / "npm"
npm_cmd = bin_dir / "npm.cmd"
extensionless.write_text("#!/usr/bin/env node\n")
npm_cmd.write_text("@echo off\n")
monkeypatch.setattr(hermes_constants.sys, "platform", "win32")
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.setenv("PATH", str(bin_dir))
assert find_node_executable("npm") == str(npm_cmd)
def test_with_hermes_node_path_prepends_existing_managed_dirs(self, tmp_path, monkeypatch):
home = tmp_path / "hermes"
node_dir = home / "node"