feat: patch ALL installed cli.js locations for multi-install support

- Add find_all_cli_js() — discovers all cli.js paths across npm global dirs,
  which claude symlink resolution, and NVM per-user installs
- Update cmd_update() to iterate all found paths and patch each one
- Update ensure_claude_code() to use find_all_cli_js() for install detection
- Fixes: `claude --version` showing old version when multiple Claude Code
  installs exist (e.g. /usr/lib vs /usr/local/lib vs NVM)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
delta-cloud-208e
2026-02-26 18:39:32 +00:00
parent ae06a26974
commit ea8ce4f05d

View File

@@ -218,7 +218,8 @@ def ensure_claude_code(target_version=None):
If target_version is set and the installed version doesn't match,
reinstall to the exact version so cli.js patch is compatible.
"""
cli_js = find_cli_js()
all_paths = find_all_cli_js()
cli_js = all_paths[0] if all_paths else None
# If already installed, check version compatibility
if cli_js and target_version:
@@ -241,8 +242,8 @@ def ensure_claude_code(target_version=None):
capture_output=True, text=True, timeout=300,
)
if result.returncode == 0:
cli_js = find_cli_js()
if cli_js:
found = find_all_cli_js()
if found:
new_ver, _ = get_installed_version()
print(f" {G}Claude Code {new_ver or ''} installed{D}")
return True
@@ -296,6 +297,58 @@ def find_cli_js():
return None
def find_all_cli_js():
"""Find ALL installed Claude Code cli.js paths (for multi-install patching)."""
candidates = set()
if IS_WINDOWS:
for env_key in ("APPDATA", "LOCALAPPDATA", "PROGRAMFILES"):
base = os.environ.get(env_key, "")
if base:
candidates.add(os.path.join(base, "npm", "node_modules",
"@anthropic-ai", "claude-code", "cli.js"))
else:
# Static well-known paths
for prefix in ("/usr/lib", "/usr/local/lib", "/opt/homebrew/lib"):
candidates.add(os.path.join(prefix, "node_modules",
"@anthropic-ai", "claude-code", "cli.js"))
# npm root -g (primary install path)
try:
r = subprocess.run(["npm", "root", "-g"], capture_output=True, text=True, timeout=10)
if r.returncode == 0:
candidates.add(os.path.join(r.stdout.strip(),
"@anthropic-ai", "claude-code", "cli.js"))
except Exception:
pass
# Resolve `which claude` → follow symlinks → find cli.js
try:
r = subprocess.run(["which", "claude"], capture_output=True, text=True, timeout=5)
if r.returncode == 0:
claude_bin = os.path.realpath(r.stdout.strip())
# claude binary is at node_modules/.bin/claude
# cli.js is in node_modules/@anthropic-ai/claude-code/cli.js
nm = os.path.dirname(os.path.dirname(claude_bin)) # node_modules/
candidates.add(os.path.join(nm, "@anthropic-ai", "claude-code", "cli.js"))
except Exception:
pass
# NVM installs: /root/.nvm, /home/*/.nvm
nvm_bases = ["/root/.nvm"]
if os.path.isdir("/home"):
for user in os.listdir("/home"):
nvm_bases.append(f"/home/{user}/.nvm")
for nvm_base in nvm_bases:
versions_dir = os.path.join(nvm_base, "versions", "node")
if os.path.isdir(versions_dir):
for node_ver in os.listdir(versions_dir):
candidates.add(os.path.join(versions_dir, node_ver, "lib",
"node_modules", "@anthropic-ai", "claude-code", "cli.js"))
return [p for p in candidates if os.path.isfile(p)]
def get_installed_version():
"""Get currently installed Claude Code version.
@@ -879,9 +932,12 @@ def cmd_update(force=False, settings_only=False):
# Install cli.js
if not settings_only:
if not cli_js:
cli_js = find_cli_js()
if not cli_js:
all_paths = find_all_cli_js()
if not all_paths:
cli_js_single = find_cli_js()
if cli_js_single:
all_paths = [cli_js_single]
if not all_paths:
eprint(f" {R}Claude Code cli.js not found even after install attempt.{D}")
return 1
@@ -890,9 +946,16 @@ def cmd_update(force=False, settings_only=False):
eprint(f" Run with: sudo python3 {sys.argv[0]}")
return 1
print(f"\n{W}--- Installing cli.js v{latest} ---{D}")
ok = install_cli_js(latest, cli_js)
if not ok:
print(f"\n{W}--- Installing cli.js v{latest} (found {len(all_paths)} location(s)) ---{D}")
any_ok = False
for path in all_paths:
print(f" Patching: {path}")
ok = install_cli_js(latest, path)
if ok:
any_ok = True
else:
eprint(f" {Y}Failed to patch {path}, continuing...{D}")
if not any_ok:
return 1
# Patch settings