Skip to content

fix(desktop): Finder launch crashed with 'spawn node ENOENT' on nvm-only machines - #290

Open
dapsychyoo wants to merge 2 commits into
vakovalskii:mainfrom
dapsychyoo:fix/desktop-node-resolve
Open

fix(desktop): Finder launch crashed with 'spawn node ENOENT' on nvm-only machines#290
dapsychyoo wants to merge 2 commits into
vakovalskii:mainfrom
dapsychyoo:fix/desktop-node-resolve

Conversation

@dapsychyoo

Copy link
Copy Markdown

Problem

Launching codbash.app from Finder crashes with:

Uncaught Exception:
Error: spawn node ENOENT

on any machine where node is installed only via nvm (no Homebrew/system node). Root cause, traced and reproduced on a real machine:

  1. A Finder launch gets PATH=/usr/bin:/bin:/usr/sbin:/sbin, so every standard candidate in resolveNodeBin() misses.
  2. The login-shell fallback ran $SHELL -lic 'command -v node' and took the last line of stdout — but rc files freely print to stdout. iTerm2 shell integration emits OSC 1337 escape sequences without a trailing newline, so the captured "line" was \x1b]1337;RemoteHost=...\x07/Users/u/.nvm/versions/node/v20.20.0/bin/nodeexistsSync() rejects it.
  3. Resolution fell through to bare 'node' → ENOENT, and startServer() had no 'error' handler on the child, so it surfaced as an Uncaught Exception dialog.

Fix

  • Extract resolution into desktop/node-resolve.js (unit-testable; main.js requires electron and can't load under node --test).
  • Reuse src/shell-path.js for the login-shell step instead of a private probe: its \x01-sentinel capture is immune to rc noise, it logs failures, and its 24h disk cache removes the ~1s interactive shell spawn from cold start. Dev: ../src; packaged: extraResources app/src (mirrors resolveServerEntry).
  • Scan ~/.nvm/versions/node/*/bin/node before spawning any shell — fixes nvm-only machines even if the probe fails. The user's ~/.nvm/alias/default (exact or prefix form, e.g. 20) is honored over the highest installed version, so the scan never silently overrides the default the prebuilt node-pty ABI was working against.
  • Handle spawn 'error' in startServer: a clear dialog (install Node 18+ or set CODBASH_NODE) instead of an uncaught exception; SMOKE mode fails loudly with exit 1; the later waitForServer catch no longer stacks a second, misleading dialog on top.
  • desktop/package.json: add node-resolve.js to build.files.

Verification

  • 14 new unit tests in test/desktop-node-resolve.test.js (TDD, red→green); full suite 270 pass / 2 pre-existing skips. Injected existsSync fakes are scoped to the fake home so the suite is independent of the host's /usr/local/bin/node.
  • On the machine that crashed: packaged-app smoke test (CODBASH_SMOKE=1) under a stripped Finder PATH (env -i ... PATH=/usr/bin:/bin:/usr/sbin:/sbin) → [desktop] SMOKE OK, resolver returns the nvm-default v20.20.0 (previously: crash).

🤖 Generated with Claude Code

https://claude.ai/code/session_01F3q8EdVFkKPTtSGhAU1Wxf

John Doe and others added 2 commits August 31, 2026 12:13
A Finder launch crashed with 'Uncaught Exception: spawn node ENOENT' on
machines where node is installed only via nvm. The login-shell fallback
took the LAST line of stdout from `$SHELL -lic 'command -v node'`, but rc
files freely print to stdout — iTerm2 shell integration emits OSC 1337
escape sequences without a trailing newline, so the node path came back
glued to escape garbage, existsSync() rejected it, and the app fell
through to bare 'node', which the stripped GUI PATH cannot find.

- Extract resolution into desktop/node-resolve.js (unit-testable; main.js
  requires electron) and probe the login shell with the same \x01-sentinel
  technique src/shell-path.js already uses, immune to rc noise.
- Scan ~/.nvm/versions/node/*/bin/node (highest first) before spawning a
  shell at all — fixes nvm-only machines even when the probe fails, and
  skips the ~1s shell spawn on them.
- Handle spawn 'error' in startServer: a clear dialog (install Node 18+ or
  set CODBASH_NODE) instead of an uncaught exception.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3q8EdVFkKPTtSGhAU1Wxf
…ult, guard dialogs

- Drop the duplicated sentinel probe: reuse src/shell-path.js's cached,
  logged captureLoginShellPath() (dev: ../src, packaged: extraResources
  app/src) and scan its PATH for node.
- listNvmNodes: put ~/.nvm/alias/default (exact or prefix form) first so
  the scan never silently overrides the user's default with a newer
  install (prebuilt node-pty ABI may not match); drop the double stat —
  the candidate loop owns the existence check via the injected seam.
- startServer 'error' handler: return early during a quit in progress;
  in SMOKE mode fail loudly with a nonzero exit instead of a silent
  green run.
- whenReady catch: skip the generic 'failed to start' dialog when the
  spawn error dialog already told the user what's wrong.
- Tests: scope injected existsSync fakes to the fake home so the suite
  doesn't depend on the host machine's /usr/local/bin/node (CI runners
  all have one).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3q8EdVFkKPTtSGhAU1Wxf
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.

1 participant