Repository navigation
fix(install): find programs on PATH without which, and count a symlinked PATH dir once - #804
Merged
sirmalloc merged 9 commits intoNov 6, 2026
Merged
Conversation
…which The TUI asked `which` whether npm, npx, bun and bunx are installed and where ccstatusline resolves. `which` is a separate package that minimal Linux images (Fedora, RHEL/UBI) and a base Arch install leave out. Without it every lookup failed the way a missing tool does: the Install screen marked all four package managers "not installed" and disabled every option, update actions were disabled too, and Manage Installation said ccstatusline is not resolvable on PATH. When `which` can't be started, the lookup now searches PATH itself and lists what `which -a` would: each file the user can run, in PATH order. `command -v` could answer "is npm installed" but can't list every match, which the multiple-binaries warning needs, so both checks share this one search. The availability check now goes through the same lookup as the ccstatusline resolution, so the two can't disagree. Where `which` exists nothing changes, and Windows still uses `where`.
The "Multiple ccstatusline binaries are on PATH" warning counts the directories `which -a` lists ccstatusline in, comparing them as text. On merged-/usr Linux (Ubuntu 22.04+, Debian 12) /bin is a symlink to usr/bin and the default PATH lists both, so with npm's global prefix at /usr one installed binary shows up as /usr/bin/ccstatusline and /bin/ccstatusline. Manage Installation and the install and update notices then warned about a second install that doesn't exist. Both duplicate checks now compare each directory with its symlinks resolved, and fall back to the path as listed when it can't be resolved. The warning still shows the paths as PATH spells them.
The PATH search used when there is no `which` kept the process.env object that existed when its module loaded. Code that replaces process.env with a new object afterwards (a test restoring a saved environment does) left the search reading the old object, so a PATH set on the new one was never seen. The default search now reads process.env each time it runs.
…arning The warning counts installs by directory, with symlinks resolved, but built its "Other matches" list from every path `which -a` printed after the first. With a real second install on a merged-/usr system, where /bin links to usr/bin, PATH order gives /usr/bin/ccstatusline, /bin/ccstatusline and then the other one, so the list named /bin/ccstatusline as another binary although it is the first match reached a second way. The list now takes the first path listed in each directory and leaves out the first match's directory, so each other install appears once, spelled as PATH lists it.
…de and Bun The duplicate-binary checks resolve each PATH directory with fs.realpathSync before comparing. Where the file system ignores letter case (macOS by default), Bun's realpathSync returns the case stored on disk but Node's returns the case it was given. A PATH that spells one directory two ways, such as .../bin and .../Bin, therefore showed "Multiple ccstatusline binaries are on PATH" when the TUI ran under Node and no warning under Bun. The lookup now uses fs.realpathSync.native, which asks the operating system and returns the on-disk case under both runtimes. A directory that can't be resolved still falls back to the path as listed.
…iling The lookup searches PATH itself only when `which` can't be started (ENOENT). When `which` runs and exits non-zero, it found nothing, and that answer stands. No test covered this: with the ENOENT check removed, every failed `which -a ccstatusline` would fall through to the PATH search, and the tests still passed. The new case has `which -a` exit with status 1 while PATH holds an executable ccstatusline, and expects no matches.
… realpath The letter-case test passes under Bun with or without the native call, because Bun's plain realpathSync already reports the on-disk case, and it is skipped where the file system is case-sensitive. A second test checks the call itself, so a revert fails under bun test on any system.
findAllOnPosixPath spelled its default search as an object literal inside the parameter list, which made the signature hard to read at a glance. The default now comes from defaultPosixPathSearch(), next to the isExecutableFile check it uses. A default parameter is evaluated on every call that leaves it out, so the helper still builds a fresh search from process.env as it is when the search runs: an env object swapped in after the module loaded is still seen.
# Conflicts: # src/utils/__tests__/global-package-manager.test.ts
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.
What
Two fixes to how the TUI looks for programs on PATH:
whichbinary: on systems without one, the TUI no longer reports npm, npx, bun and bunx as missing or says ccstatusline isn't on PATH./binand/usr/binon merged-/usr Linux, orbinandBinon a case-insensitive macOS volume under Node) no longer triggers "Multiple ccstatusline binaries are on PATH". When there is a real second install, "Other matches" lists it once instead of also naming the first match's other spelling.Why (evidence)
No
which. The Install screen askswhich <tool>whether npm, npx, bun and bunx exist, and Manage Installation askswhich -a ccstatuslinewhere the command resolves.whichis its own package, and minimal Fedora/RHEL/UBI images and a base Arch install don't include it. When it's missing, the lookup fails exactly as if the tool were missing:which, the availability check returns{"npm":false,"npx":false,"bun":false,"bunx":false}under both Bun and Node.npm install -g ccstatusline@2.2.32 (npm not installed),bun add -g … (bun not installed),npx -y ccstatusline@latest (npx not installed)andbunx … (bunx not installed), and none of them can be selected. The TUI can't install at all, even though the user just started it with npx.sh -c 'which npm'exits 127 (which: command not found) whilecommand -v npmfinds npm.Symlinked PATH directories. The multiple-binaries warning counts the directories
which -alists, comparing them as text. On Ubuntu 22.04+ and Debian 12,/binis a symlink tousr/binand the default PATH lists both. With npm's global prefix at/usr(NodeSource's Node, for example),which -a ccstatuslineprints/usr/bin/ccstatuslineand/bin/ccstatusline, which are the same file. Manage Installation and the install/update notices then show "⚠ Multiple ccstatusline binaries are on PATH … Other matches: /bin/ccstatusline". Reproduced with ausr/bindirectory and abin -> usr/binsymlink both on PATH. With a real second install listed after those two, "Other matches" named/bin/ccstatuslineas well as the other install, because the list took every path after the first. On macOS, Node'sfs.realpathSynckeeps the letter case it is given while Bun's returns the case on disk. With one scratch directory on PATH as both.../binand.../Bin, whichwhich -alists twice, the check behind the install and update notices warned under Node v26.10.0 and not under Bun 1.4.2.How
whichcan't be started (ENOENT),getCommandResolutionPathssearches PATH itself through the newfindAllOnPosixPathinexecutable-path.ts: each file the user can run, in PATH order, aswhich -alists them. Empty and relative PATH entries are skipped: they name directories relative to wherever the TUI runs, which says nothing about what Claude Code runs, and the Windows lookup in the same file already counts only absolute entries.command -v. It could answer "is npm installed" but can't list every match, which the multiple-binaries warning needs, so both checks share one PATH search.whichexists nothing changes (a normal "not found" still returns nothing), and Windows keeps usingwhere.getRealDirectory, which falls back to the listed path when it can't be resolved). It callsfs.realpathSync.native, which returns the on-disk letter case under both runtimes. "Other matches" lists the first path in each other directory, so each other install appears once. The warning text keeps the paths as PATH spells them./usr/sbinis a symlink tobinand npm's global prefix is/usr. With Docker's default PATH (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin), a rootnpm i -g ccstatuslinetherefore resolves first as/usr/sbin/ccstatusline. On that layout this branch drops the false "Multiple ccstatusline binaries" warning but shows "ccstatusline resolves to /usr/sbin/ccstatusline, which is outside the npm global bin directory (/usr/bin)" instead. Manage Installation detects no package manager, as onmain, and says the binary is "outside the detected npm and bun global bin directories". Reproduced with a fake root of that layout under Bun and Node. Ubuntu and Debian aren't affected:/usr/sbinis a real directory there, and their default PATH lists/usr/binbefore/bin.Demo
Install to Claude Code with no
whichon PATH. Before, every package manager reads "not installed" and can't be picked; after, they're listed normally.Powerline
Before:
After:
Plain
Before:
After:
Manage Installation with one binary reached through
usr/binandbin -> usr/bin. Before, a false "Multiple ccstatusline binaries" warning; after, none.Powerline
Before:
After:
Plain
Before:
After:
Testing
On
mainas of fix(powerline): keep the Custom theme when Powerline is enabled again #793 (a0bedc0): it merges cleanly.bun run lintis clean, andbun testgives 4162 pass, 0 fail (4147 onmain). The built CLI under Bun 1.4.4 and Node 26.13.0 agrees across runtimes and matchesmainin plain and Powerline mode, and the TUI opens and exits cleanly under both. It adds no JavaScript or Node API newer than Node 14, the build's target.15 new tests:
process.envreplaced after load, and the real permission check (executable file, non-executable file, directory, symlink);whichis missing, and no fallback whenwhichruns and fails;which;The behaviour tests fail on
main, except the failing-whichcase: it keeps whatmainalready does and fails once the ENOENT check is dropped. The letter-case test fails before its fix only under Node, since Bun'srealpathSyncalready returned the on-disk case, so a second test checks that the native call is made; it fails underbun testwhen the call is reverted.Node (vitest), changed test files: the only failures are the existing "Cannot spy on export … Module namespace is not configurable in ESM" kind (7 more than
main, all new mock-based tests failing the same way); the new PATH search tests and the letter-case tests pass under Node.End to end with a PATH of node, bun, npm, npx and bunx but no
which, under both runtimes:mainshows all four managers as not installed; this branch lists them and finds ccstatusline.End to end with a merged-/usr layout under both runtimes:
mainshows the false warning; this branch doesn't.End to end on macOS with one scratch directory on PATH as
.../binand.../Bin(realwhich -a): before the letter-case fix Node warned and Bun didn't; now neither does.