Skip to content

fix(clink): resolve npm-managed CLIs when nvm PATH is missing - #455

Open
syf2211 wants to merge 2 commits into
BeehiveInnovations:mainfrom
syf2211:fix/clink-nvm-path-resolution
Open

fix(clink): resolve npm-managed CLIs when nvm PATH is missing#455
syf2211 wants to merge 2 commits into
BeehiveInnovations:mainfrom
syf2211:fix/clink-nvm-path-resolution

Conversation

@syf2211

@syf2211 syf2211 commented Jun 25, 2026

Copy link
Copy Markdown

Summary

Fixes clink failing with codex: command not found when Codex CLI is installed globally via nvm + npm, but the PAL MCP subprocess (e.g. launched by uvx) does not inherit the nvm-injected PATH.

Motivation

Issue #442 reports that clink with codex fails on Linux when Codex is installed with npm install -g @openai/codex under nvm. The MCP server process only sees a minimal PATH, so shutil.which("codex") returns None even though the binary exists under ~/.nvm/versions/node/<version>/bin.

Changes

  • Add clink/path_utils.py with helpers to collect common npm/node version-manager bin directories (nvm, fnm, volta, asdf, mise, ~/.npm-global/bin)
  • Use resolve_executable() instead of bare shutil.which() in BaseCLIAgent.run()
  • Augment subprocess PATH in _build_environment() so node-based CLIs can find their runtime
  • Add unit tests for PATH augmentation and nvm resolution
  • Update existing clink agent tests to mock resolve_executable

Tests

python3 -m pytest tests/test_clink_path_utils.py tests/test_clink_codex_agent.py tests/test_clink_claude_agent.py tests/test_clink_gemini_agent.py -q
# 12 passed

Notes

  • Targets Unix-style nvm layout ($NVM_DIR/versions/node/*/bin); nvm-windows uses different paths and is out of scope for this issue.
  • When multiple nvm versions are installed without a default alias, all version bin dirs are prepended (newest lexicographically first).

Fixes #442

When PAL MCP runs via uvx, the subprocess often lacks nvm/fnm node bin
directories in PATH, causing clink to fail with 'codex: command not found'
even though the CLI is installed globally via npm.

Augment PATH with common version-manager bin directories before resolving
executables and pass the augmented PATH to CLI subprocesses.

Fixes BeehiveInnovations#442

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces PATH augmentation helpers in clink/path_utils.py to resolve npm-managed CLI executables (such as those managed by nvm, fnm, or volta) and integrates them into the base CLI agent. Feedback highlights a bug in sorting Node.js version directories lexicographically rather than semantically, which can lead to older versions being preferred. Additionally, there is a redundant call to augment_path when resolving executables in clink/agents/base.py because the environment's PATH is already augmented; using shutil.which directly would prevent duplicate filesystem operations and simplify imports.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread clink/path_utils.py Outdated
Comment on lines +35 to +36
for node_bin in sorted(versions_dir.glob("*/bin"), reverse=True):
candidates.append(str(node_bin))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation sorts the Node.js version directories lexicographically using sorted(..., reverse=True). Lexicographical sorting of version strings does not align with semantic versioning (e.g., "v9.0.0" is lexicographically greater than "v22.1.0" because '9' > '2'). This can cause older Node.js versions to be incorrectly preferred over newer ones when no default alias is set.

We should use a custom sorting key that parses the version components as integers to ensure correct semantic version ordering.

Suggested change
for node_bin in sorted(versions_dir.glob("*/bin"), reverse=True):
candidates.append(str(node_bin))
import re
def version_key(p: Path) -> list[int]:
return [int(x) for x in re.findall(r"\d+", p.parent.name)]
for node_bin in sorted(versions_dir.glob("*/bin"), key=version_key, reverse=True):
candidates.append(str(node_bin))

Comment thread clink/agents/base.py Outdated
# Resolve executable path for cross-platform compatibility (especially Windows)
executable_name = command[0]
resolved_executable = shutil.which(executable_name)
resolved_executable = resolve_executable(executable_name, path=env.get("PATH"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In _build_environment(), env["PATH"] is already augmented with augment_path(env.get("PATH")). Calling resolve_executable(executable_name, path=env.get("PATH")) here results in a redundant call to augment_path(), which performs duplicate filesystem I/O (scanning directories, reading files, etc.).

Since env["PATH"] is already fully augmented, we can directly use shutil.which() to resolve the executable path efficiently.

        import shutil
        resolved_executable = shutil.which(executable_name, path=env.get("PATH"))

Comment thread clink/agents/base.py Outdated
from clink.constants import DEFAULT_STREAM_LIMIT
from clink.models import ResolvedCLIClient, ResolvedCLIRole
from clink.parsers import BaseParser, ParsedCLIResponse, ParserError, get_parser
from clink.path_utils import augment_path, resolve_executable

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since we can use shutil.which() directly in BaseCLIAgent.run() to avoid redundant PATH augmentation, we only need to import augment_path from clink.path_utils.

Suggested change
from clink.path_utils import augment_path, resolve_executable
from clink.path_utils import augment_path

- Sort nvm version directories by semantic version, not lexicographically
- Use shutil.which on already-augmented PATH to avoid redundant I/O
- Add test for semantic version ordering
@syf2211

syf2211 commented Jun 25, 2026

Copy link
Copy Markdown
Author

Thanks for the review! Addressed both points in aaa867d:

  1. Semantic version sorting — nvm version directories are now sorted with an integer-based version_key instead of lexicographic order (added regression test for v9.0.0 vs v22.1.0).
  2. Redundant augment_pathBaseCLIAgent.run() now uses shutil.which(executable_name, path=env.get("PATH")) directly since PATH is already augmented in _build_environment().

All clink path_utils tests pass locally.

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.

[Bug] clink "codex: command not found" when Codex CLI is installed globally via nvm + npm

2 participants