Skip to content

fix(installer): symlink bundled node/npm into command bin dir for FHS root installs - #38889

Merged
teknium1 merged 1 commit into
mainfrom
fix/node-symlink-fhs-root-install
Jun 4, 2026
Merged

teknium1 merged 1 commit into
mainfrom
fix/node-symlink-fhs-root-install

Conversation

@alt-glitch

@alt-glitch alt-glitch commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

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. Result:
node/npm are "command not found" and hermes dashboard fails with
"npm is not available".

Root Cause

install_node() (scripts/install.sh:845) hardcodes $HOME/.local/bin for the
node symlinks. But get_command_link_dir() — the helper the hermes command
already uses — correctly returns /usr/local/bin for root FHS installs.
PR #15608 created the FHS path for the command but left install_node pointed
at the legacy user-local dir.

The PATH-guard (install.sh:1538+) doesn't self-heal this because it checks
$command_link_dir (= /usr/local/bin, already on PATH), so it's a no-op for
the exact case that needs it.

Fix

  • scripts/install.sh: install_node() now symlinks into
    get_command_link_dir() — same helper the hermes command link uses.
    /usr/local/bin on FHS root, ~/.local/bin otherwise, $PREFIX/bin on Termux.
  • scripts/lib/node-bootstrap.sh: new _nb_get_link_dir() mirrors the
    same logic for the standalone bootstrap path (used by hermes update, TUI
    node bootstrap).
  • hermes_cli/uninstall.py: remove_node_symlinks() now checks all
    candidate directories so root FHS uninstalls don't leave orphan symlinks in
    /usr/local/bin.

Non-root and Termux installs are unchanged (same symlink target as before).

Reproduction & Verification

Tested on a clean Ubuntu 24.04 VM (libvirt, cloud image):

Step Before (bug) After (fix)
Fresh root install (curl | bash) node → /root/.local/bin/ (not on PATH) node → /usr/local/bin/ (on PATH ✓)
command -v node in stock root shell NOT FOUND /usr/local/bin/node
hermes dashboard "npm is not available" builds web UI, serves HTTP 200 ✓

Test Plan

  • Existing 6 uninstall tests pass
  • New test_removes_fhs_symlinks_in_usr_local_bin covers FHS path
  • Clean VM end-to-end: root install → node resolves → hermes dashboard builds + serves

Infographic

PR #38889 — installer node bin dir fix

… 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.
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: fix/node-symlink-fhs-root-install vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9788 on HEAD, 9788 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5081 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@teknium1
teknium1 merged commit aeec88c into main Jun 4, 2026
23 checks passed
@teknium1
teknium1 deleted the fix/node-symlink-fhs-root-install branch June 4, 2026 09:31
@alt-glitch

Copy link
Copy Markdown
Contributor Author

Review response — hardening + scope decisions

Thanks for the sharp review. Addressed the high/medium items in commit 2ff73853e, verified the two you said to hammer (#1 migration, dashboard build) on a throwaway VM, and made explicit scope calls on the rest.

Fixed

#1 Existing-broken-install → update/migration (the trap). Confirmed and fixed. ensure_node() (node-bootstrap.sh) and check_node() (install.sh) both early-returned when a bundled node already existed, only fixing the current shell's PATH and never re-creating the /usr/local/bin symlinks — so a previously-broken root box stayed broken after hermes update. Both now call a shared link_bundled_node / _nb_link_bundled_node that idempotently re-links into the canonical dir.

#2 Stale ~/.local/bin orphans. The re-link helpers now prune stale node/npm/npx links in the other candidate dirs (target-checked: only links resolving into this hermes home's node dir are removed — never a real binary or an nvm/fnm link). So a migrated root box no longer keeps shadowing copies in ~/.local/bin.

#4 Parity. _nb_get_link_dir() now mirrors resolve_install_layout()'s legacy-install carve-out (root user with $HERMES_HOME/hermes-agent/.git keeps ~/.local/bin), so the bootstrap path can't link node to a different dir than the installer put the command. Also consolidated the duplicated layout logic into one canonical helper in hermes_constants (command_link_dir / command_link_candidate_dirs / bundled_node_bin_dir / find_node_executable), now consumed by doctor, profiles, uninstall, backup, main.

Doctor now catches this regression class (it previously couldn't): hermes doctor reports "Node.js installed but not on PATH" instead of a false "not found", verifies the /usr/local/bin/node symlink on root FHS, and the npm-audit block no longer silently vanishes when npm is off-PATH. Also fixed doctor's own command-link detection (was hardcoded ~/.local/bin, would create a wrong duplicate symlink with --fix on root FHS).

Bonus FHS bugs found in the audit + fixed: profile-alias wrappers (hermes profile) were hardcoded to ~/.local/bin → off-PATH for root FHS; now layout-aware, and removal scans all candidate dirs. Defensive bundled-node fallback added to the dashboard web-UI build, WhatsApp bridge, and LSP installer.

Verified (throwaway Ubuntu 24.04 VM, libvirt)

A scenario matrix (fresh-root / migration / fresh-nonroot / uninstall), run against the patched install.sh:

  • fresh-root ✓ node resolves in a stock non-login root shell, symlink in /usr/local/bin, none left in ~/.local/bin, hermes dashboard serves HTTP 200, web_dist built, tsc present.
  • migration ✓ install → simulate old broken layout (links only in ~/.local/bin, off root PATH; precondition asserts node is genuinely unreachable) → re-run installer → node restored to /usr/local/bin, stale ~/.local/bin/node pruned.

#7 dashboard tsc: not found

Does not reproduce on a fresh root install — node_modules/.bin/tsc is properly hoisted with the bundled node v22 and the build completes to HTTP 200. The original report was a corrupted/partial node_modules from an interrupted install, not a layout defect. The web-UI build now also injects the bundled node bin dir into the build subprocess PATH so tsc/vite (which re-invoke node) work even from a stripped-PATH context.

Deliberately scoped OUT (separate issues, will file follow-ups)

Unit tests: +9 hermes_constants helper tests, +4 profiles wrapper-dir tests; existing doctor/profiles/backup/uninstall/constants suites all green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants