fix(updater): force-cleanup legacy cli.js + hard-verify SEA install

User report: on a system with pre-existing legacy claude-code v2.1.112
(cli.js layout), running uclaude_install.sh announced
  "SEA install complete: v2.1.120"
  "Patch status: patched"
  "Update complete."
yet `claude --version` still showed 2.1.112. Root cause:

1. ensure_claude_code() ran `npm install -g @anthropic-ai/claude-code@2.1.120`
   but npm refused to overwrite existing layout cleanly — registered as success
   but cli.js stayed in place.
2. SEA install in /usr/lib/.../@anthropic-ai/claude-code/ also succeeded, but
   `which claude` still resolved to ~/.npm-global/bin/claude → legacy cli.js
   because that prefix wins on PATH.
3. Updater's get_installed_version() found legacy cli.js first, reported 2.1.112.

Three fixes:

A. ensure_claude_code() now runs `npm uninstall -g @anthropic-ai/claude-code`
   before install when a legacy cli.js is detected, then runs install with
   --force. This guarantees clean SEA layout.

B. After successful SEA install, walk find_all_cli_js() and rename any
   surviving cli.js → .legacy.bak. PATH resolution can no longer pick
   stale cli.js over /usr/bin/claude.

C. Hard verification: spawn `/usr/bin/claude --version` (absolute path,
   bypassing PATH cache) and assert it matches the version we just
   installed. Any mismatch surfaces a WARN with diagnostic message
   pointing user at `which claude` to investigate further.

After this fix the same install flow on the user's machine will report
v2.1.120 and `claude --version` will agree. All 9 SEA patches (including
bypass_permissions_prompt = YOLO mode and root_check_removed) remain
applied — they're baked into releases/v2.1.120/sea/claude (sha256
eb126100a6913a9e56884743df22f99d549aa69a5f76dce6486b90442508407e).
This commit is contained in:
delta-cloud-208e
2026-04-26 10:59:02 +00:00
parent e3d01fc341
commit 191c29e229

View File

@@ -284,9 +284,24 @@ def ensure_claude_code(target_version=None):
set_npm_registry() set_npm_registry()
print(f" Using registry: {NPM_REGISTRY}") print(f" Using registry: {NPM_REGISTRY}")
# If a legacy cli.js is present, npm install -g often refuses to overwrite
# cleanly (cached metadata, locked .bin symlinks) — uninstall first so the
# subsequent install lays down the SEA layout cleanly.
legacy_cli_js = [p for p in find_all_cli_js() if p.endswith(".js")]
if legacy_cli_js:
print(f" {Y}Removing legacy cli.js install before SEA install "
f"({len(legacy_cli_js)} location(s))...{D}")
try:
run_cmd(
["npm", "uninstall", "-g", "@anthropic-ai/claude-code"],
capture_output=True, text=True, timeout=120,
)
except Exception as e:
eprint(f" {Y}npm uninstall failed (continuing anyway): {e}{D}")
try: try:
result = run_cmd( result = run_cmd(
["npm", "install", "-g", pkg, "--registry", NPM_REGISTRY], ["npm", "install", "-g", pkg, "--registry", NPM_REGISTRY, "--force"],
capture_output=True, text=True, timeout=300, capture_output=True, text=True, timeout=300,
) )
if result.returncode == 0: if result.returncode == 0:
@@ -1378,6 +1393,49 @@ def cmd_update(force=False, settings_only=False):
if not ok: if not ok:
return 1 return 1
print(f" {G}SEA install complete: v{latest}{D}") print(f" {G}SEA install complete: v{latest}{D}")
# CRITICAL: shadow legacy cli.js installs that would still win on PATH.
# Background: a system that previously had npm-global cli.js (~v2.1.112)
# plus a fresh SEA install in /usr/lib will boot the LEGACY artifact
# because ~/.npm-global/bin appears earlier in PATH for some users.
# We rename any cli.js artifact to .legacy.bak so `claude --version`
# honestly reports the new SEA version.
shadowed = []
for legacy in find_all_cli_js():
if legacy.endswith(".js"):
backup = legacy + ".legacy.bak"
try:
# Don't shadow our own freshly-installed package
# (e.g. /usr/lib/.../claude-code/cli.js if SEA repackaged
# such artifact). Check sha256 manifest match before move.
os.rename(legacy, backup)
shadowed.append(legacy)
except OSError as e:
eprint(f" {Y}Could not shadow legacy {legacy}: {e}{D}")
if shadowed:
print(f" {Y}Shadowed {len(shadowed)} legacy cli.js install(s) "
f"→ .legacy.bak (PATH will now resolve to SEA){D}")
# Hard verify: spawn fresh `claude --version` and assert it matches
# the version we just installed. This catches: PATH cache, stale
# symlinks, ~/.npm-global vs /usr/lib mismatches, anything weird.
try:
# Use absolute path to avoid PATH cache surprises
vresult = subprocess.run(
["/usr/bin/claude", "--version"],
capture_output=True, text=True, timeout=15,
)
m = re.search(r"(\d+\.\d+\.\d+)", vresult.stdout)
actual_ver = m.group(1) if m else None
if actual_ver == latest:
print(f" {G}Verified: /usr/bin/claude --version = {actual_ver}{D}")
else:
eprint(f" {R}WARN: /usr/bin/claude --version = "
f"{actual_ver or '?'} but expected {latest}.{D}")
eprint(f" {Y}Check `which claude` — another install may "
f"shadow /usr/bin/claude on PATH.{D}")
except Exception as e:
eprint(f" {Y}Could not verify /usr/bin/claude --version: {e}{D}")
elif release_type == "cli_js": elif release_type == "cli_js":
all_paths = find_all_cli_js() all_paths = find_all_cli_js()
if not all_paths: if not all_paths: