Skip to content

fix(tui): keep npm-prefix updates on the Node pinned by the receipt - #445

Merged
hetaoBackend merged 3 commits into
mainfrom
fix/homebrew-node-launcher-pin
Oct 9, 2026
Merged

hetaoBackend merged 3 commits into
mainfrom
fix/homebrew-node-launcher-pin

Conversation

@yujiachen-y

@yujiachen-y yujiachen-y commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Change

Fixes the TUI side of #446 (CLI fails to start after brew upgrade removes the pinned Homebrew Node keg).

Installer side: tracked in #457. The upcoming installer release records the formula-managed bin/node symlink instead of the Cellar keg, so reinstalling after brew upgrade restores a working launcher directly; recording the opt/<formula>/bin/node path per #457 is the remaining refinement, and existing installs converge on the opt path through the in-app update migration added here.

On macOS, a Homebrew-managed Node resolves through the stable bin/node symlink into a versioned Cellar keg such as Cellar/node/26.7.0/bin/node. The npm-prefix update flow pinned release launchers to process.execPath, which is that versioned keg path. After brew upgrade removes the old keg, the pinned launcher execs a path that no longer exists and the CLI fails to start with a bare "not found" (exit 127), with no self-healing path.

  • npm-prefix updates now reuse the nodeExecutable recorded in install.json when it still exists, so launcher activation, the npm child process, and SQLite validation stay on the pinned executable instead of re-resolving it through process.execPath. Receipts without a usable nodeExecutable keep the previous behavior.

  • Versioned launchers print an actionable repair hint (re-run the installer) and exit 127 when their pinned Node is missing, instead of a bare exec failure.

  • Same-major Homebrew upgrades keep the Node ABI stable, so following the formula-managed symlink is safe; cross-major drift still surfaces through the existing SQLite validation and installer repair flow.

  • PR labels: bug, cli, tui

  • 386b7ce (review follow-up): update-runtime.test.ts 9/9 passed, adding a Cellar-receipt E2E that heals to the Homebrew opt/<formula>/bin/node path (receipt + launcher assertions, no /Cellar/ remains), a missing-receipt-Node fallback E2E, and unit coverage for relative / missing / directory nodeExecutable plus a schemaVersion 1 receipt; pnpm run typecheck and ESLint on changed files passed.

  • Checks run and results (revision 129e3e8, local macOS, Node 22.22):

    • node scripts/run-vitest-suite.mjs capability / focused files: new packages/tui/test/unit/update-runtime.test.ts 6/6 passed, including an end-to-end case that pins a Homebrew-style symlink from the receipt, verifies the activated launcher and new receipt keep the symlink (not process.execPath), and checks the exit-127 repair hint after the symlink is removed; sibling update-application.test.ts / update-service.test.ts 87 passed, 3 skipped.
    • pnpm run typecheck, pnpm run build, pnpm run lint:tui, pnpm run check:source, pnpm run check:tsconfig: passed.
    • release/public-source.json regenerated via node scripts/source-inventory.mjs --write after adding the test file; new test declared in test/vitest-suites.json (capability gate).
  • Performance: basic.

  • NOT RUN, platform limitations and live-service boundaries:

    • Full local pnpm verify capability gate: 17 test files in local-runtime / local-runtime-v2 / repo-level suites fail on this machine, reproduced identically on clean main with this branch stashed (pre-existing local-environment failures, unrelated to the changed files). Relying on CI for the full matrix.
    • No real brew upgrade exercised on a physical Homebrew Node install; the keg-removal scenario is covered by the simulated fixture. No live-service or Windows-specific validation run locally.

Publication and contribution checks

  • I have permission to contribute these changes under the existing licenses applicable to the changed files/packages; imported material and its provenance are identified and existing notices are preserved.
  • No credentials, account data, real user content, internal source history or private review material is included.
  • Added/removed source files were reviewed before regenerating release/public-source.json; new tests are declared in test/vitest-suites.json where applicable.
  • Shared English/Chinese documentation and capability/verification records are updated where applicable. Mock/offline results are not described as live-service acceptance.

Maintainer handoff

Publication scope or license changes (if any): none.

Shared-source port: complete for the TUI update flow covered here; installer-side distribution changes ride the regular installer release process.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

The versioned npm-prefix update flow pins every release launcher to one
Node executable so the better-sqlite3 binding stays ABI-consistent, and
records that executable in install.json. The launcher runtime, however,
was taken from process.execPath: on macOS, execPath resolves Homebrew's
stable bin/node symlink into a versioned Cellar keg such as
Cellar/node/26.7.0/bin/node. After `brew upgrade` removes the old keg,
self-updated launchers exec a path that no longer exists and the CLI
fails to start with a bare "not found".

Reuse the nodeExecutable recorded in the install receipt when it is
still present, so launcher activation and the npm/npm validation child
processes stay on the pinned executable instead of re-resolving it
through process.execPath. Receipts without a usable nodeExecutable keep
the previous process.execPath behavior. Versioned launchers now also
print an actionable repair hint and exit 127 when their pinned Node is
missing, instead of a bare exec failure.

Homebrew same-major upgrades keep the ABI stable, so following the
formula-managed symlink is safe; a cross-major upgrade surfaces through
the existing SQLite validation and installer repair flow.

Assisted-by: pi
@yujiachen-y yujiachen-y added bug Something isn't working cli Standalone mcode: TUI, headless, ACP and source builds/tooling tui Interactive terminal UI (TUI) labels Oct 8, 2026

@hetaoBackend hetaoBackend left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes. The TUI side looks right and CI is green, but on its own this doesn't fix existing Homebrew installs.

Blocking: the live install.sh resolves Node to process.execPath on purpose (L983, "not a version-manager symlink") and writes it to install.json (L958). Homebrew receipts therefore hold Cellar/node/<ver>/bin/node, and preferring the receipt's nodeExecutable still breaks after brew upgrade. With a realistic Cellar receipt I still get exit 127, now with the hint. The E2E test passes only because its receipt stores a symlink, which the installer never writes.

Both of these are needed:

  1. Installer: for Homebrew only, record <brew>/opt/<formula>/bin/node. Keep the realpath for fnm, asdf and volta, whose links are per-shell or per-directory. Users who already hit 127 can't start mcode and can only rerun the installer, which today pins Cellar again.
  2. TUI migration as a complement: if the receipt's realpath is under <brew>/Cellar/<formula>/<ver>/bin/node and <brew>/opt/<formula>/bin/node resolves to the same file, switch to the opt path, so existing installs heal on their next update.

Prefer opt/<formula>/bin over <brew>/bin/node. brew link --overwrite can repoint bin/node, and createMcodeNpmRuntimeEnvironment prepends the Node directory to PATH, which would put all of /opt/homebrew/bin ahead of npm and the better-sqlite3 build.

Non-blocking:

  • After a cross-major Node upgrade, the better-sqlite3 load error suggests npm install -g … --foreground-scripts (cli/main.ts:213-232), which is wrong for npm-prefix installs. It should point to rerunning the installer.
  • install-source.ts:652 uses only existsSync. Please also check that the path is a file and executable (statSync().isFile() + accessSync(X_OK)).
  • The Windows .cmd/.ps1 launchers have no missing-Node hint. Fine as a follow-up.
  • Tests to add: a receipt nodeExecutable that is missing or relative (falls back to process.execPath), a schemaVersion 1 receipt, and a receipt that already holds a Cellar path.
  • Please link or open an issue with repro steps.

Review follow-up for npm-prefix updates on Homebrew-managed Node.

Receipts written by earlier installers record the resolved Cellar keg
path, so simply preferring the receipt's nodeExecutable still breaks
after `brew upgrade` removes the keg. Canonicalize the recorded path at
receipt parse time: when it points into a versioned Cellar/node keg (or
a Homebrew-managed bin/node) and the formula's stable
opt/<formula>/bin/node resolves to the same executable, use the opt path
instead. The opt directory also keeps the update PATH prefix scoped to
the Node formula rather than all of <brew>/bin. Unusable entries
(missing, relative, non-file, non-executable) fall back to
process.execPath as before.

Also point the native SQLite startup diagnostic at rerunning the
installer for npm-prefix installs instead of an npm-global reinstall
command.

Assisted-by: pi
@yujiachen-y

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review — all blocking points addressed in 386b7ce.

Blocking

  • Receipt reuse alone doesn't heal existing installs — agreed, and now fixed on the TUI side: receipt parsing canonicalizes the recorded Node before reuse. Paths shaped <prefix>/Cellar/<node@x>/<version>/bin/node (prefix-agnostic, Cellar is Homebrew-specific) and Homebrew-managed <brew-prefix>/bin/node / opt/<formula>/bin/node receipts are healed to <prefix>/opt/<formula>/bin/node when both sides resolve to the same executable (realpathSync equality). The new receipt and launchers written at activation then record the opt path, so existing installs converge on their next in-app update.
  • opt over bin — agreed, and the migration applies it: bin/node receipts under a real Homebrew prefix (/opt/homebrew, /usr/local, /home/linuxbrew/.linuxbrew) also migrate to opt/node/bin/node, which keeps createMcodeNpmRuntimeEnvironment's PATH prefix scoped to the Node formula instead of all of <brew>/bin.
  • Installer recording the stable path — that change is not part of this repository; it ships with the installer release process. The TUI migration above covers receipts already in the wild either way, and the launcher hint (exit 127, "re-run the MCode installer") covers users who can no longer start mcode at all.

Non-blocking

  • cli/main.ts SQLite diagnostic: npm-prefix installs now get "Reinstall with the original installer to repair this installation." instead of the npm install -g … --foreground-scripts suggestion (that hint remains for npm-global installs).
  • install-source.ts usability check: statSync().isFile() + accessSync(X_OK) in addition to the absolute-path check; unusable entries (missing, relative, directory) fall back to process.execPath.
  • Tests added: Cellar-receipt E2E (asserts receipt + launcher pin the opt path, no /Cellar/ remains, launcher runs), missing-receipt-Node fallback E2E, and unit coverage for relative / missing / directory nodeExecutable and a schemaVersion 1 receipt.
  • Issue with repro steps opened and linked: CLI fails to start after brew upgrade removes the pinned Homebrew Node keg #446.
  • Windows .cmd/.ps1 missing-Node hint: kept out of this PR; happy to follow up separately.

Validation on 386b7ce (local macOS, Node 22.22): update-runtime.test.ts 9/9, sibling update-application/update-service 96 passed + 3 skipped, pnpm run typecheck, ESLint on changed files — all green; CI running.

@yujiachen-y

Copy link
Copy Markdown
Collaborator Author

@hetaoBackend PTAL

@hetaoBackend

Copy link
Copy Markdown
Collaborator

Thanks, 386b7ce addresses the review: the TUI migration from Cellar to opt/<formula>/bin/node looks right, and the non-blocking items and tests are covered (repro in #446).

Two things before approval:

  1. Installer: since install.sh lives outside this repo, could you open a tracking issue (or link the installer PR) for recording <brew>/opt/<formula>/bin/node there? Until that ships, users who already hit exit 127 and rerun the installer get a Cellar path again, and only heal on the next in-app update. Please mention this in the PR description too.
  2. Small nit: the formula branch in HOMEBREW_MANAGED_NODE_PATH is missing opt/, so it matches <brew>/node/bin/node and is never reached. The result is still correct because opt paths are returned unchanged, but please fix the pattern or drop the branch.

Once the installer follow-up is tracked and the nit is fixed, I'm happy to approve.

Copy link
Copy Markdown
Collaborator

Installer follow-up is now tracked in #457.

Still waiting on the small HOMEBREW_MANAGED_NODE_PATH nit from the previous comment (formula branch is missing opt/, so it matches <brew>/node/bin/node instead of opt/<formula>/bin/node). Once that is fixed, happy to approve.

Review follow-up: the formula branch of HOMEBREW_MANAGED_NODE_PATH
was missing the opt/ literal, so it matched the nonexistent
<brew>/node/bin/node while real opt/<formula>/bin/node receipts never
matched. opt receipts are still returned unchanged either way, but the
pattern now recognizes them explicitly and the escaped linuxbrew
segment stops matching lookalike prefixes. Adds a pass-through test
for opt receipts.

Assisted-by: pi
@yujiachen-y

Copy link
Copy Markdown
Collaborator Author

Nit fixed in 299822a: the formula branch now matches opt/<formula>/bin/node (including keg-only node@<major> receipts), <brew>/node/bin/node no longer matches, and the linuxbrew prefix segment is properly escaped. Added a pass-through test for opt receipts; update-runtime.test.ts 9/9 and pnpm run typecheck pass locally.

Installer side: thanks for opening #457 — PR description now links it. One clarification on its Context section: the next installer release records the formula-managed bin/node symlink rather than the Cellar keg, so reinstalling after brew upgrade already restores a working launcher without waiting for an in-app update; recording opt/<formula>/bin/node per #457 is the remaining refinement. Existing installs written by the currently-live installer heal through this PR's update migration either way.

@hetaoBackend hetaoBackend left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the opt/ regex fix and the pass-through test look good, and the installer follow-up is tracked in #457. LGTM.

@hetaoBackend
hetaoBackend merged commit f466590 into main Oct 9, 2026
13 checks passed
@hetaoBackend
hetaoBackend deleted the fix/homebrew-node-launcher-pin branch October 9, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cli Standalone mcode: TUI, headless, ACP and source builds/tooling tui Interactive terminal UI (TUI)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants