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: