Skip to content

feat(doctor): detect when jaw is unreachable from a non-interactive shell (#479) - #485

Merged
lidge-jun merged 1 commit into
devfrom
codex/479-remote-path
Aug 26, 2026
Merged

feat(doctor): detect when jaw is unreachable from a non-interactive shell (#479)#485
lidge-jun merged 1 commit into
devfrom
codex/479-remote-path

Conversation

@parkjs101

Copy link
Copy Markdown
Collaborator

What

jaw doctor now reports when jaw cannot be resolved from a non-interactive shell — the condition behind #479, which is invisible from the shell doctor itself runs in.

Why this was invisible

ssh host 'jaw serve ...' runs a non-login, non-interactive shell that reads none of the files scripts/install.sh writes to:

Shell Installer writes to Read by ssh host 'cmd'
bash .bashrc, .bash_profile ✗ — .bash_profile is login-only, and Debian's stock .bashrc returns on its first line when non-interactive
zsh .zshrc, .zprofile ✗ — non-interactive zsh reads only .zshenv
other .profile ✗ — login-only

So ~/.local/bin never reaches PATH, nohup fails to resolve jaw, and the only evidence is No such file or directory — a message that never mentions PATH.

A naive check could not catch this either: doctor runs in the interactive shell where PATH already works. The check therefore asks the question against getconf PATH.

Verification

$ jaw doctor
⚠️  Non-interactive PATH (ssh): jaw is in /Users/syon/.nvm/versions/node/v22.19.0/bin but not on the
   non-interactive PATH — remote `ssh host 'jaw ...'` will fail with "No such file or directory"
     call it by absolute path: …/bin/jaw
     or export PATH in the remote command: …
     or (zsh) add to ~/.zshenv: …

$ env -i PATH="$(getconf PATH)" sh -c 'command -v jaw'
(nothing — the warning is accurate)

$ env -i PATH="$(getconf PATH)" sh -c 'export PATH="…/bin:$PATH"; command -v jaw'
…/bin/jaw          # the printed remedy works

Running it live also caught a real defect before merge: the first version searched only a static list of install dirs, so a version-managed Node (nvm/fnm/Homebrew) read as "not installed" and the check stayed silent on a host that is broken. Candidate dirs now include the caller's resolved npm prefix and binary dir.

11 unit tests (tests/unit/noninteractive-path.test.ts), tsc --noEmit clean, full suite shows 0 new failures vs an origin/dev baseline.

Refs #479

Stack (merge bottom-up):

# Layer Review focus
4 docs — remote/headless guide docs only
3 supervisor backend generated sh + liveness contract
2 service guidance operator message correctness
1 doctor detection ← you are here detection logic + probe seam

Review this PR's diff only; its base is the layer below.

…hell

`ssh host 'jaw serve ...'` runs a non-login, non-interactive shell that reads
none of the files scripts/install.sh writes to: bash skips .bash_profile and
.profile (login-only) and Debian's stock .bashrc returns at its first line when
non-interactive, while zsh reads only .zshenv, which the installer never
touches. So ~/.local/bin never makes it onto PATH and the remote command dies
with "nohup: failed to run command 'jaw'" — a message that names no PATH.

doctor could not see this before, and a naive check could not either: doctor
runs in the interactive shell where PATH already works. The check therefore
asks the question against `getconf PATH` (what a non-interactive shell
actually starts from) rather than process.env.PATH.

Candidate install dirs include the caller's own resolved locations (npm prefix,
the running binary's dir) because a version-managed Node puts global bins under
a prefix no static list can enumerate. Verified on a host with nvm: the check
reports unreachable, and `env -i PATH="$(getconf PATH)" sh -c 'command -v jaw'`
confirms it resolves nothing.

Refs #479
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.

3 participants