fix: find CLIs installed by npm's global prefix, not just the PATH one - #5824
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Installing the Codex CLI from the AI Providers card succeeded and PortOS still reported:
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-wideC:\ProgramData\npm\npmwhile PATH carries onlyC:\Program Files\nodejsand the per-user%APPDATA%\npmdefault — socodex.cmdwas written somewhere nothing on PATH names, and no restart could inherit a directory the machine PATH never contained. The same gap hits anNPM_CONFIG_PREFIXan admin set, an nvm/Volta switch, or aprefix=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%/$HOMEguesses 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 ontoprocess.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 asadoptPathDirs(dirs), shared withllamaServerManager'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.services/bootstrap.jsandcos-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 atnpm prefix -ginstead 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, perproviderRuntimeInstaller's existing contract.Test plan
server/lib/npmGlobalBin.test.js(new) — single probe shared by concurrent callers;nullwhen 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.js—adoptPathDirsappends 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.processEnv,npmGlobalBin,lib/index,providerRuntimeInstaller,providers.runtimeInstall,llamaServerManager,providerReadiness,providerPrerequisites).findCommandOnPath('codex')goesnull→C:\ProgramData\npm\npm\codex.CMD, and the runtime status flips toinstalled: 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