From 191c29e2290769066bf8e7061df2670c251cc08b Mon Sep 17 00:00:00 2001 From: delta-cloud-208e Date: Sun, 26 Apr 2026 10:59:02 +0000 Subject: [PATCH] fix(updater): force-cleanup legacy cli.js + hard-verify SEA install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- claude/uclaude_updater.py | 60 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/claude/uclaude_updater.py b/claude/uclaude_updater.py index 767d0fb..ee4965e 100755 --- a/claude/uclaude_updater.py +++ b/claude/uclaude_updater.py @@ -284,9 +284,24 @@ def ensure_claude_code(target_version=None): set_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: 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, ) if result.returncode == 0: @@ -1378,6 +1393,49 @@ def cmd_update(force=False, settings_only=False): if not ok: return 1 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": all_paths = find_all_cli_js() if not all_paths: