Skip to content

fix: find CLIs installed by npm's global prefix, not just the PATH one - #5824

Merged
atomantic merged 2 commits into
mainfrom
cos/task-mtjospef/agent-b9470507
Sep 2, 2026
Merged

fix: find CLIs installed by npm's global prefix, not just the PATH one#5824
atomantic merged 2 commits into
mainfrom
cos/task-mtjospef/agent-b9470507

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Installing the Codex CLI from the AI Providers card succeeded and PortOS still reported:

The installer finished, but PortOS still cannot run codex. Its bin directory may be missing from PortOS's PATH — restart PortOS, then try again.

Restarting never helped, because the advice named the one remedy that cannot work.

Root cause. npm writes global binaries to npm prefix -g, which is not necessarily a directory the host's Node installer put on PATH. On the reporting machine the prefix is a machine-wide C:\ProgramData\npm\npm while PATH carries only C:\Program Files\nodejs and the per-user %APPDATA%\npm default — so codex.cmd was written somewhere nothing on PATH names, and no restart could inherit a directory the machine PATH never contained. The same gap hits an NPM_CONFIG_PREFIX an admin set, an nvm/Volta switch, or a prefix= in a user or global npmrc.

npm resolves its prefix through a config cascade (cli flags, npm_config_*, project/user/global npmrc, and a builtin npmrc that itself interpolates env vars), so deriving it from %APPDATA%/$HOME guesses would reproduce the bug on the next host. Only npm can answer.

Changes

  • server/lib/npmGlobalBin.js (new) — asks npm once (npm prefix -g, cached as a promise so concurrent callers share one spawn) and adopts the answer onto process.env.PATH. The on-disk check is deliberately not cached with npm's answer: the prefix directory does not exist until the first global install, and making that install visible without a restart is the point.
  • server/lib/processEnv.js — the PATH-adoption mechanics land here as adoptPathDirs(dirs), shared with llamaServerManager's winget-shim adoption, which was a second copy of the same read-split-filter-append and had drifted: it deduped case-sensitively, so on Windows it could append a directory PATH already carried. It now inherits the case-insensitive compare.
  • Boot adoption in BOTH processes that spawn provider CLIsservices/bootstrap.js and cos-runner/index.js. The CoS runner is its own PM2 app with its own environment; without its own adoption a CLI the Providers page reports as installed would still 422 there as "not on the CoS Runner PATH", so the fix would have lied about the runner.
  • providerRuntimeInstaller.js — adopts again from the runtime probe, because on a first install the prefix directory did not exist when boot looked.
  • routes/providers.js — the post-install failure message now points at npm prefix -g instead of advising a restart that cannot help.

Extending process.env.PATH (rather than just resolving a path) is load-bearing: a TUI provider is launched by bare name through node-pty, which has nothing but PATH to go on. The status payload still carries no filesystem paths, per providerRuntimeInstaller's existing contract.

Test plan

  • server/lib/npmGlobalBin.test.js (new) — single probe shared by concurrent callers; null when npm cannot be asked (no %APPDATA%-style guess fallback); a prefix that only appears after the first global install is adopted without a re-probe.
  • server/lib/processEnv.test.jsadoptPathDirs appends a real directory and skips a nonexistent one, is idempotent, and matches an existing Windows PATH entry regardless of case.
  • server/services/providerRuntimeInstaller.test.js — the probe adopts before resolving.
  • 193 tests pass across the 9 affected suites (processEnv, npmGlobalBin, lib/index, providerRuntimeInstaller, providers.runtimeInstall, llamaServerManager, providerReadiness, providerPrerequisites).
  • Verified live on the reporting machine: findCommandOnPath('codex') goes nullC:\ProgramData\npm\npm\codex.CMD, and the runtime status flips to installed: true.

Full server suite: 37289 passing. The 8 unrelated failing files are Python-missing environment failures (tts-qwen3, imageTo3d, videoGen, image_execution_marker) plus two that pass in isolation under full-parallel load; none import anything this branch touches.

🤖 Generated with Claude Code

Installing the Codex CLI from the AI Providers card succeeded and PortOS
still reported "the installer finished, but PortOS still cannot run
`codex`. Its bin directory may be missing from PortOS's PATH — restart
PortOS, then try again." Restarting never helped.

npm writes global binaries to `npm prefix -g`, which is not necessarily a
directory the host's Node installer put on PATH. On the reporting machine
the prefix is a machine-wide `C:\ProgramData\npm\npm` while PATH still
carries only `C:\Program Files\nodejs` and the per-user `%APPDATA%\npm`
default — so `codex.cmd` was written somewhere nothing on PATH names, and
no restart could inherit a directory the machine PATH never contained.
The same gap hits an `NPM_CONFIG_PREFIX` an admin set, an nvm/Volta
switch, or a `prefix=` in a user or global npmrc.

npm resolves its prefix through a config cascade (cli flags, npm_config_*,
project/user/global npmrc, and a builtin npmrc that itself interpolates
env vars), so deriving it from %APPDATA%/$HOME guesses would reproduce the
bug on the next host. New `server/lib/npmGlobalBin.js` asks npm once and
adopts the answer onto `process.env.PATH`, which fixes this process and
every child it spawns — the only fix that reaches a TUI provider, which
node-pty launches by bare name.

Adoption runs at boot in BOTH processes that spawn provider CLIs (the
server and the CoS runner, which has its own environment and would
otherwise still refuse a CLI the Providers page reports as installed), and
again from the runtime probe, because on a first install the prefix
directory did not exist when boot looked.

The PATH-adoption mechanics move into `processEnv.js` as `adoptPathDirs`,
shared with llamaServerManager's winget-shim adoption — which also gains
the case-insensitive Windows compare it was missing. The post-install
failure message now points at `npm prefix -g` instead of advising a
restart that cannot help.
It was exported with one call site, and its env?.Path fallback is
unreachable — process.env is case-insensitive on Windows, so .PATH
already reads a Path entry, and adoptPathDirs was the only caller.
@atomantic
atomantic merged commit d23df78 into main Sep 2, 2026
7 checks passed
@atomantic
atomantic deleted the cos/task-mtjospef/agent-b9470507 branch September 2, 2026 07:08
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