11 Commits

Author SHA1 Message Date
Brooklyn Nicholson
8e08a4a16e fix(install): restore installs — engines floor no shipping toolchain can meet
Fresh installs and `hermes update` both fail at the first `npm ci`:

    npm error code EBADENGINE
    npm error notsup Required: {"node":">=26.0.0","npm":">=12.0.0"}
    npm error notsup Actual:   {"node":"v24.15.0","npm":"11.12.1"}

`.npmrc` sets engine-strict=true, so `engines` is a hard gate on every
install. The floor was raised to npm >=12 — but no Node release bundles
npm 12: Node 26 ships 11.17.0, 24 ships 11.16.0, 22 ships 10.9.8. The
requirement is unsatisfiable by any stock toolchain, so the installer
provisions a Node and is immediately unable to install with it.

engines.npm becomes `<11.10.0 || >=11.17.0`. That still excludes the band
the strictness was actually for: npm 11.10-11.16 honor `min-release-age`
but ignore `min-release-age-exclude`, both set in .npmrc, so they apply the
14-day gate to packages we exempted. Verified rather than assumed — npm
11.12.1 fails `ETARGET ... vite@8.2.0 with a date before 7/18/2026` while
11.17.0 installs it.

engines.node returns to >=20.0.0 and the toolchain floor to Node 22.
Nothing in the tree needs 26: Vite 8.2.0 declares `^20.19.0 || >=22.12.0`
and Electron 40 declares `>=12.20.55`. Requiring 26 force-migrated every
working install for no dependency reason. apps/desktop drops to Vite's own
floor for the same reason; the desktop bundle builds clean on Node 22.

install.sh gained a second gate: a system Node was accepted on version
alone, so a machine with Node 24 + its bundled npm 11.16.0 (the bad band)
passed the check and then failed `npm ci`. npm_supports_npmrc() now rejects
that band and installs the managed Node instead.

CI, Docker and nix are hermetic and keep pinning Node 26 / npm 12 — they
provision their own toolchain, and both satisfy the relaxed range.

tests/test_engines_satisfiable.py encodes the invariants that would have
caught this: the npm floor must be met by an npm some shipping Node
bundles, the node floor by the runtime install.sh provisions, the desktop
floor by its own build toolchain, and the lockfile mirror must match.
Restoring the broken values fails 5 of them with the reason stated.

Verified end-to-end (real downloads, temp HERMES_HOME):
- fresh install: managed node v22.23.2 / npm 10.9.8 -> npm ci, 209 packages
- existing managed tree (v22.22.3 / npm 10.9.8) -> npm ci, 209 packages
- node 26.5.1 + bundled npm 11.17.0 -> npm ci, 208 packages
- system npm 11.12.1 (the reported case) -> EBADENGINE, recovery provisions
  a managed tree and retries green
- apps/desktop `npm run build` on Node 22 -> dist built, assert passes
2026-08-01 22:56:52 -05:00
Brooklyn Nicholson
abb84c4cdf fix(install): upgrade npm on the managed-Node reuse path too
_nb_ensure_bundled_npm_range ran only at the tail of
_nb_install_bundled_node, so it fired just after a tarball was unpacked.
ensure_node's reuse rung returns before reaching it, leaving an existing
managed tree on whatever npm its Node major bundled.

That strands a real install: the upgrade is best-effort (`|| true`), so
one offline run leaves an at-target Node 26 tree carrying npm 11.17.0 —
below the root package.json's `engines.npm` floor of >=12, fatal under
.npmrc's engine-strict. Heal does not cover it either; the tree is at the
target major and every binary passes --version, so
_nb_managed_node_needs_heal correctly reports it healthy. Re-running the
installer, the documented recovery, never repaired it.

install.ps1 already had this right: Update-ManagedNpm is called from both
branches that yield a managed tree, including the reuse path. This is the
POSIX side of that same call site.

Reproduced on a seeded node-26.5.1/npm-11.17.0 tree: before, ensure_node
left npm at 11.17.0 and `npm ci` died with EBADENGINE; after, it upgrades
to 12.0.2 and `npm ci` installs 208 packages. An already-in-range tree
costs one --version probe (~0.13s), and the system-node path is unchanged.

Co-authored-by: ethernet8023 <arilotter@gmail.com>
2026-08-01 21:22:00 -05:00
ethernet
6fdc64efcd fix(install): install npm 12 into the vendored Node 26 tree
The bundled-Node bootstrap unpacked the nodejs.org tarball and stopped.
Node 26.5.1 bundles npm 11.17.0, one minor below the root package.json's
own `engines.npm` floor of >=12 — and .npmrc sets `engine-strict=true`,
so that is fatal rather than a warning:

    npm error code EBADENGINE
    npm error notsup Required: {"node":">=26.0.0","npm":">=12.0.0"}
    npm error notsup Actual:   {"node":"v26.5.1","npm":"11.17.0"}

A brand-new install died at the first `npm ci` with "Desktop workspace
npm install failed". CI never saw it because the workflows run an
explicit `npm i -g npm@12`; the Python update path recovers through
hermes_cli/npm_engine.py, but the installer path had no such rung.

_nb_ensure_bundled_npm_range() now upgrades the managed tree's npm into
range right after the tarball lands, mirroring upgrade_managed_npm():

  - temp cwd, so the checkout's own .npmrc (engine-strict,
    min-release-age) does not gate the upgrade meant to satisfy it;
  - npm_config_min_release_age=0, which also neutralises a user ~/.npmrc;
  - explicit --prefix at the managed tree, because
    _nb_configure_npm_prefix writes prefix=~/.local into its etc/npmrc
    and a bare `npm i -g` would install a second npm elsewhere while the
    managed tree stayed stale.

The range is read out of package.json rather than duplicated, so the two
cannot drift, with HERMES_NPM_TARGET_RANGE as an override and a >=12.0.0
fallback for a stripped install tree. An already-in-range npm skips the
network round-trip. Best-effort: a failed upgrade warns with the manual
command and keeps the working Node, since npm_engine.py still covers the
EBADENGINE that follows.

Verified against a real tree provisioned by this bootstrap: node v26.5.1
/ npm 12.0.2, bin/npm and bin/npx still relative-symlinked into the
upgraded lib/node_modules/npm, the ~/.local/bin links resolving to 12.0.2
through the tree, and no stray second npm under ~/.local/lib.
2026-08-01 22:00:26 -04:00
ethernet
b13148d354 feat(runtime): heal outdated managed Node trees up to the target major
Existing users who only ever launch Hermes (never re-run an installer)
kept their managed Node 22 tree forever: the heal path only fired for
*broken* trees, and a healthy 22 passes the --version probe. Now
"outdated" heals the same way "broken" does, on both sides of the mirror:

- hermes_constants.py: find_hermes_node_executable() checks
  _managed_node_tree_outdated() (managed node major <
  _HERMES_NODE_TARGET_MAJOR) and routes through the existing
  once-per-process heal_hermes_managed_node(), which redownloads
  latest-v26.x. When the heal fails (offline, download error) the
  outdated-but-runnable tree is still returned — old Node beats no Node.
- scripts/lib/node-bootstrap.sh: _nb_managed_node_needs_heal() gains the
  matching _nb_managed_node_outdated() rung, so heal_managed_node agrees
  with the Python side.

This is the same shape as the managed-uv flow: resolve the managed
runtime, notice it can't satisfy the requirement, provision the right one
in place, fall back gracefully.

Tests (tests/test_hermes_constants.py): outdated tree triggers heal and
returns the upgraded binary; failed heal still serves the old tree; an
at-target tree never heals (heal stub raises).
2026-08-01 21:17:51 -04:00
ethernet
713a983e4a feat(runtime)!: require Node 26 across all installers, heal, and upgrade paths
Hermes now pins its toolchain to Node 26 everywhere. Every path that
installs, accepts, heals, or upgrades a Node runtime moves from the old
22-default / `^20.19 || >=22.12` floor to a single rule: Node >=26.

Installers:
- scripts/install.sh — NODE_VERSION=26; node_satisfies_build() collapses
  the two-branch Vite floor to `major >= 26`; user-facing messages updated.
- scripts/install.ps1 — $NodeVersion=26; Test-NodeVersionOk likewise;
  winget fallback switches OpenJS.NodeJS.LTS -> OpenJS.NodeJS (26 is
  Current, not LTS — the LTS manifest would reinstall a too-old Node).
- Dockerfile — node_source stage node:22-bookworm-slim -> node:26 (digest
  pinned, amd64 sha256:9e6f...bf73).
- nix/ was already on nodejs_26 (lib.nix, npm-12-0-2.nix); the checks.nix
  wrapper check ratchets from `>= 20` to `>= 26`.

Heal/upgrade paths:
- scripts/lib/node-bootstrap.sh — HERMES_NODE_TARGET_MAJOR default 22->26
  and HERMES_NODE_MIN_VERSION default 20->26, so heal_managed_node,
  _nb_install_bundled_node, and the fnm/proto/nvm/brew rungs all target 26
  and stop accepting an on-PATH Node below it. Both remain env-overridable.
- hermes_constants.py — _HERMES_NODE_TARGET_MAJOR fallback 22->26, which
  drives the Windows heal path's latest-v26.x download.

Version gates:
- package.json engines.node >=20 -> >=26; apps/desktop engines
  `^20.19.0 || >=22.12.0` -> `>=26.0.0`.
- CI setup-node: all five workflows 22 -> 26.
- Docs describing Hermes's own toolchain updated (windows-native, docker,
  acp, nix-setup, contributing). Skill docs describing third-party tools'
  own requirements are untouched.

Termux still installs via `pkg install nodejs` best-effort (nodejs.org
ships no Android tarballs); that path was never version-gated.

Verified: bash -n on both shell scripts, PowerShell AST parse of
install.ps1, latest-v26.x index resolves (node-v26.5.1), and the install
test suite — 18 tests across the 5 install/runtime test files — passes.
2026-08-01 21:17:51 -04:00
Teknium
6b519255ea fix(update): provision a managed Node runtime when system npm fails engines.npm
The npm 12 requirement (f88ed6c717) strands every system-Node install:
no shipping Node bundles npm >=12, engine-strict makes EBADENGINE fatal,
and the recovery in npm_engine.py refuses to touch a system npm — so
'hermes update' leaves the install in a mixed state (updated code, stale
Node deps, no TUI/web/desktop rebuild) with only a manual-fix hint.

Instead of modifying the user's toolchain (still never done), the
EBADENGINE recovery now provisions Hermes' own managed Node tree under
$HERMES_HOME/node — the same pinned-nodejs.org path install.sh and
install.ps1 use — upgrades THAT npm into the required range, and hands
the caller the managed npm for its single retry.

- hermes_constants.bootstrap_hermes_managed_node(): cross-platform
  provisioning (POSIX via node-bootstrap.sh _nb_install_bundled_node,
  Windows via the existing portable-zip download); reuses a healthy tree.
- node-bootstrap.sh: HERMES_NODE_SKIP_LINKS=1 skips the ~/.local/bin
  node/npm/npx symlinks so the private tree never shadows the user's
  own toolchain on PATH.
- maybe_repair_npm_engine() now returns the npm path to retry with
  (managed-in-place upgrade or freshly provisioned runtime); both call
  sites retry with the returned path and put the managed tree first on
  PATH so npm lifecycle scripts resolve the managed node.
- Node-only mismatches on a foreign npm are now also recoverable (the
  managed tree ships a supported Node); on a managed npm they still
  correctly decline.

E2E (real download, temp HERMES_HOME): provisioned node v22.23.2,
upgraded bundled npm 10.9.4 -> 12.0.2, system npm byte-identical after,
no ~/.local/bin links re-pointed, healthy-tree reuse in 0.05s.
2026-08-01 16:40:50 -07:00
xxxigm
65be0061e0 fix(hermes): heal broken managed Node tree instead of PATH fallback
When a Hermes-managed node/npm/npx shim exists but fails --version, redownload
the pinned nodejs.org bundle under HERMES_HOME/node and retry. Do not fall
back to system npm on PATH when a managed tree is present.

POSIX heal probes node, npm, and npx (npm can break while node still runs).
2026-06-26 20:10:20 +05:30
xxxigm
1db8f7ea80 fix(install): repair existing managed-Node global prefix on re-run
The initial fix only wrote the prefix npmrc on a fresh Node install, so
pre-existing bundled-Node installs (Node already present) were not repaired
by re-running the installer — install_node/ensure_node skip when Node is
already up to date.

Extract the redirect into an idempotent helper
(configure_managed_node_npm_prefix / _nb_configure_npm_prefix) that no-ops
when there's no Hermes-managed npm, and call it unconditionally from
check_node (install.sh) and at the top of ensure_node (node-bootstrap.sh).
Re-running the install command now repairs an affected install in place,
not just brand-new ones.
2026-06-14 17:34:11 +07:00
xxxigm
a4ee1f223d fix(install): make npm install -g packages reachable on PATH
When the installer falls back to a bundled Node under $HERMES_HOME/node,
npm's default global prefix is that Node dir, so `npm install -g <pkg>`
drops the package binary in $HERMES_HOME/node/bin. Only node/npm/npx are
symlinked into the command link dir (~/.local/bin, /usr/local/bin, or
$PREFIX/bin) — so user-installed global package binaries are NOT on PATH
and can't be run, even though `npm i -g` reports success. They also get
wiped on every Node upgrade (the dir is rm -rf'd and re-extracted).

Redirect the bundled Node's npm global prefix to the command link dir's
parent, so global bins land in the link dir (already on PATH, alongside
node/npm/npx) and survive Node upgrades. Scoped to the bundled Node via
its prefix-local global npmrc ($HERMES_HOME/node/etc/npmrc), so the user's
other Node installs and their ~/.npmrc are untouched. Hermes's own global
installs (agent-browser) pass an explicit --prefix and are unaffected.
2026-06-14 17:21:20 +07:00
alt-glitch
aeec88c77f fix(installer): symlink bundled node/npm into command bin dir for FHS root installs
Root installs on Linux (FHS layout, #15608) put the `hermes` command in
`/usr/local/bin` (on PATH) but symlinked the bundled node/npm/npx into
`~/.local/bin`, which isn't on PATH for a stock root shell. `node`/`npm`
were 'command not found' and `hermes dashboard` failed with 'npm is not
available' because its build-on-demand fallback couldn't find npm.

Fix: `install_node()` now symlinks into `get_command_link_dir()` — the same
helper the `hermes` command link already uses — so node/npm/npx land
wherever the command does (`/usr/local/bin` on FHS root, `~/.local/bin`
otherwise, `$PREFIX/bin` on Termux). Non-root and Termux installs are
unchanged.

Also fixes:
- `scripts/lib/node-bootstrap.sh`: adds `_nb_get_link_dir()` mirroring
  the same root/Termux/user logic for the standalone bootstrap path
  (used by `hermes update`, TUI node bootstrap, etc.)
- `hermes_cli/uninstall.py`: `remove_node_symlinks()` now checks all
  candidate directories (`~/.local/bin`, `/usr/local/bin`, `$PREFIX/bin`)
  so root FHS uninstalls don't leave orphan symlinks

Regression from #15608, which created the FHS path for the command but
left `install_node` pointed at the legacy user-local dir.
2026-06-04 02:31:49 -07:00
Brooklyn Nicholson
c4b9750bc1 feat: lazy bootstrap node 2026-04-16 10:47:37 -05:00