fix(hermes): fall back when managed node/npm fails health probe
A stale or partial Hermes-managed Node tree under the active HERMES_HOME can leave bin/npm behind while lib/cli.js is missing. File-existence checks alone made hermes update pick that broken npm and skip healthy system npm on PATH. Probe managed candidates with --version before preferring them.
This commit is contained in:
parent
7b2c51152a
commit
9274f73e48
1 changed files with 44 additions and 4 deletions
|
|
@ -277,8 +277,44 @@ def _candidate_node_command_names(command: str) -> list[str]:
|
|||
return [f"{base}.cmd", f"{base}.exe", base]
|
||||
|
||||
|
||||
def node_tool_runnable(path: str | None) -> bool:
|
||||
"""Return True only when *path* is a Node/npm/npx binary that actually runs.
|
||||
|
||||
Hermes-managed Node trees live under ``$HERMES_HOME/node`` (or a profile's
|
||||
``HERMES_HOME``). A partial upgrade or interrupted install can leave
|
||||
``bin/npm`` behind while ``lib/cli.js`` is missing — the wrapper exists but
|
||||
immediately throws ``MODULE_NOT_FOUND``. ``find_hermes_node_executable``
|
||||
used to trust file presence alone, so ``hermes update`` would pick that
|
||||
broken npm and skip the healthy system one on PATH.
|
||||
|
||||
Probe with ``--version`` (same pattern as :func:`agent_browser_runnable`) so
|
||||
callers can fall through to the next resolution candidate.
|
||||
"""
|
||||
if not path:
|
||||
return False
|
||||
candidate = Path(path)
|
||||
if sys.platform == "win32":
|
||||
if not candidate.is_file():
|
||||
return False
|
||||
elif not os.path.exists(path) or not os.access(path, os.X_OK):
|
||||
return False
|
||||
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[path, "--version"],
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
env=with_hermes_node_path(),
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired, ValueError):
|
||||
return False
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def find_hermes_node_executable(command: str) -> str | None:
|
||||
"""Return a Hermes-managed Node/npm executable path, if installed."""
|
||||
"""Return a Hermes-managed Node/npm executable path, if installed and runnable."""
|
||||
names = _candidate_node_command_names(command)
|
||||
for directory in iter_hermes_node_dirs():
|
||||
for name in names:
|
||||
|
|
@ -286,7 +322,9 @@ def find_hermes_node_executable(command: str) -> str | None:
|
|||
if candidate.is_file() and (
|
||||
sys.platform == "win32" or os.access(candidate, os.X_OK)
|
||||
):
|
||||
return str(candidate)
|
||||
resolved = str(candidate)
|
||||
if node_tool_runnable(resolved):
|
||||
return resolved
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -319,10 +357,12 @@ def find_node_executable_on_path(command: str) -> str | None:
|
|||
|
||||
|
||||
def find_node_executable(command: str) -> str | None:
|
||||
"""Resolve a Node.js command, preferring Hermes-managed installs.
|
||||
"""Resolve a Node.js command, preferring healthy 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.
|
||||
missing, or elevation-triggering system Node/npm on PATH. A managed tree
|
||||
that exists on disk but fails a ``--version`` probe is skipped so PATH can
|
||||
supply a working system install instead.
|
||||
"""
|
||||
return find_hermes_node_executable(command) or find_node_executable_on_path(command)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue