#!/usr/bin/env bash# Install, update or remove the Claude Code CLI (`claude`) the AI commands drive.# ===============================================================================## Installs, updates, or removes the Claude Code CLI (`claude`) — Anthropic's# agentic coding tool. This project uses it for the `claude-solver` shell# command and the in-shell `claude-euler-solver` skill (see packages/solver/solver/ai/skill.py).## Installation uses Anthropic's native installer (https://claude.ai/install.sh),# which places a self-contained `claude` binary under ~/.local/bin — no Node.js# and no sudo required. If `curl` is unavailable it falls back to a global npm# install of @anthropic-ai/claude-code.## Features:# - Checks for an existing Claude Code installation (idempotent install)# - Installs the latest stable version via the native installer (npm fallback)# - Updates an existing install in place# - Links the project-root `.claude/` and `CLAUDE.md` to the canonical copies# under packages/solver/solver/ai/claude/ (idempotent; any pre-existing file is backed up,# never deleted)# - Uninstalls cleanly (binary, local install data, optional user config)# - Warns if ~/.local/bin is not on PATH## Author: Vikas Munshi <vikas.munshi@gmail.com># Copyright (c) 2026. All rights reserved.# Licensed under the MIT License.set-euopipefailINSTALLER_URL="https://claude.ai/install.sh"NPM_PACKAGE="@anthropic-ai/claude-code"LOCAL_BIN="${HOME}/.local/bin"CLAUDE_BIN="${LOCAL_BIN}/claude"CLAUDE_DATA_DIR="${HOME}/.local/share/claude"# Project root, derived from this script's location (scripts/setup/claude_code.sh),# so the symlink helpers work regardless of the caller's current directory.SCRIPT_DIR="$(cd"$(dirname"${BASH_SOURCE[0]}")"&&pwd)"PROJECT_ROOT="$(cd"${SCRIPT_DIR}/../.."&&pwd)"usage(){echo"Usage: $0 [install|update|uninstall|status]"echoecho" install Install Claude Code if missing (default if no argument given)"echo" update Update an existing install to the latest version"echo" uninstall Remove the Claude Code binary and local install data"echo" status Show installation state and version"}# Returns 0 if the `claude` command is resolvable on PATH, 1 otherwise.claude_is_installed(){command-vclaude&>/dev/null}# Print the installed version using the freshly installed binary, tolerating a# PATH that does not yet include ~/.local/bin in the current shell.claude_version(){PATH="${LOCAL_BIN}:${PATH}"claude--version2>/dev/null||echo"unknown"}# Warn (don't fail) if ~/.local/bin — where the native installer drops the# binary — is not on PATH, so the user knows why `claude` isn't found yet.print_path_hint(){case":${PATH}:"in*":${LOCAL_BIN}:"*);;*)echo"Note: ${LOCAL_BIN} is not on your PATH."echo" Add it, e.g.: echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc";;esac}# Installs Claude Code via the native installer, falling back to npm.install_claude(){ifclaude_is_installed;thenecho"Claude Code is already installed: $(claude--version2>/dev/null||echopresent)"return0fiecho"Installing Claude Code..."ifcommand-vcurl&>/dev/null;thencurl-fsSL"${INSTALLER_URL}"|bashelifcommand-vnpm&>/dev/null;thenecho"curl not found; falling back to npm..."npminstall-g"${NPM_PACKAGE}"elseecho"Error: need either curl (preferred) or npm to install Claude Code.">&2return1fiecho"Claude Code installation completed: $(claude_version)"print_path_hint}# Updates an existing Claude Code install in place (installs it if missing).update_claude(){if!claude_is_installed;thenecho"Claude Code is not installed; installing instead..."install_claudereturn0fiecho"Updating Claude Code..."ifclaudeupdate2>/dev/null;then:# native self-updater handled itelifcommand-vcurl&>/dev/null;thencurl-fsSL"${INSTALLER_URL}"|bashelifcommand-vnpm&>/dev/null;thennpminstall-g"${NPM_PACKAGE}"fiecho"Claude Code now at: $(claude_version)"}# Removes the Claude Code binary, local install data, and (with confirmation)# the user configuration. Also removes a global npm install if present.uninstall_claude(){if!claude_is_installed&&[!-e"${CLAUDE_BIN}"]&&[!-d"${CLAUDE_DATA_DIR}"];thenecho"Claude Code does not appear to be installed"return0fiifcommand-vnpm&>/dev/null&&npmls-g"${NPM_PACKAGE}"&>/dev/null;thenecho"Removing global npm package ${NPM_PACKAGE}..."npmuninstall-g"${NPM_PACKAGE}"||truefiif[-e"${CLAUDE_BIN}"];thenecho"Removing ${CLAUDE_BIN}"rm-f"${CLAUDE_BIN}"fiif[-d"${CLAUDE_DATA_DIR}"];thenecho"Removing ${CLAUDE_DATA_DIR}"rm-rf"${CLAUDE_DATA_DIR}"fiif[-d"${HOME}/.claude"];thenread-r-p"Remove Claude Code user configuration (~/.claude)? [y/N] "replyif[["${reply}"=~^[Yy]$]];thenecho"Removing Claude Code user configuration under ~/.claude"rm-rf"${HOME}/.claude"elseecho"Keeping Claude Code user configuration"fifiecho"Claude Code uninstallation completed"}# Create one relative symlink ${PROJECT_ROOT}/<name> -> <target>, idempotently.# If the path is already that symlink, nothing happens. Anything else in the way# (a real file/dir, or a symlink pointing elsewhere) is moved aside to a# timestamped backup rather than deleted, then the link is created. A missing# target is skipped with a warning.link_one(){localname="$1"target="$2"locallink="${PROJECT_ROOT}/${name}"localabs_target="${PROJECT_ROOT}/${target}"if[!-e"${abs_target}"];thenecho"Warning: link target ${target} is missing; skipping ${name}">&2return0fi# The link's text: the target relative to the link's own directory, so the# checkout still resolves after it is moved, copied or mounted elsewhere.localrel_targetrel_target="$(realpath-m--relative-to="$(dirname"${link}")""${abs_target}")"# Already the symlink we want — nothing to do.if[-L"${link}"]&&["$(readlink"${link}")"="${rel_target}"];thenreturn0fi# A symlink to the right place, spelled absolutely: rewrite it in place —# there is nothing to preserve.if[-L"${link}"]&&["$(realpath-m"${link}")"="$(realpath-m"${abs_target}")"];thenln-sfn"${rel_target}""${link}"echo"Relinked ${name} -> ${rel_target}"return0fi# Something else is in the way (real file/dir, or a symlink to another target,# or a dangling symlink). Preserve it: move it aside, never delete.if[-e"${link}"]||[-L"${link}"];thenlocalbackupbackup="${link}.bak.$(date+%Y%m%d%H%M%S)"echo"Backing up existing ${name} -> ${backup##*/}"mv"${link}""${backup}"filn-s"${rel_target}""${link}"echo"Linked ${name} -> ${rel_target}"}# Rewrite an absolute symlink as the same link spelled relative to its own# directory. The native installer points ${CLAUDE_BIN} at an absolute# /home/<user>/... path, which dangles once the user id (and so the home# directory) is renamed; the relative form survives it. Anything other than an# absolute symlink is left alone. `claude update` writes the absolute form back,# so this runs after every install and update.relativize_link(){locallink="$1"destrel[-L"${link}"]||return0dest="$(readlink"${link}")"case"${dest}"in/*);;*)return0;;esac# -s: keep the logical path — resolving symlinks would bake in today's layout.rel="$(realpath-ms--relative-to="$(dirname"${link}")""${dest}")"ln-sfn"${rel}""${link}"echo"Relinked ${link} -> ${rel}"}# Expose the package's Claude Code config at the project root, where the `claude`# CLI and IDE integrations expect `.claude/` and `CLAUDE.md`. The canonical copies# are version-controlled under packages/solver/solver/ai/claude/; these are relative symlinks to# them (recreated here in case a checkout did not preserve them). The installer's# ${CLAUDE_BIN} link is made relative too, so a user id rename does not break it.link_project_config(){link_one".claude""packages/solver/solver/ai/claude"link_one"CLAUDE.md""packages/solver/solver/ai/claude/CLAUDE.md"relativize_link"${CLAUDE_BIN}"}# Reports the state of one project-root symlink for `status`.status_link(){localname="$1"target="$2"locallink="${PROJECT_ROOT}/${name}"if[-L"${link}"];thenecho" ${name}: ✓ -> $(readlink"${link}")"elif[-e"${link}"];thenecho" ${name}: ✗ present but not a symlink"elseecho" ${name}: ✗ missing (run install to create it)"fi}# Reports whether Claude Code is installed, where, and its version.status_claude(){ifclaude_is_installed;thenecho"Claude Code: ✓ installed ($(command-vclaude))"echo" version: $(claude--version2>/dev/null||echounknown)"elseecho"Claude Code: ✗ not installed"print_path_hintfiecho"Project config links:"status_link".claude""packages/solver/solver/ai/claude"status_link"CLAUDE.md""packages/solver/solver/ai/claude/CLAUDE.md"}# Main execution — defaults to 'status' if no argument provided.ACTION="${1:-status}"case"${ACTION}"ininstall)install_claudelink_project_config;;update)update_claudelink_project_config;;uninstall)uninstall_claude;;status)status_claude;;-h|--help|help)usage;;*)echo"Unknown action: ${ACTION}"usageexit1;;esac