Created
March 26, 2026 15:03
-
-
Save sorrycc/f403e2f7966708c1accb472b6510148b to your computer and use it in GitHub Desktop.
Claude Code Version Manager - list, install, and remove Claude Code versions interactively
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -euo pipefail | |
| GCS_BUCKET="https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases" | |
| GCS_API="https://storage.googleapis.com/storage/v1/b/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/o" | |
| VERSIONS_DIR="$HOME/.local/share/claude/versions" | |
| # Colors | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| CYAN='\033[0;36m' | |
| BOLD='\033[1m' | |
| DIM='\033[2m' | |
| RESET='\033[0m' | |
| # Detect platform | |
| detect_platform() { | |
| local os arch | |
| case "$(uname -s)" in | |
| Darwin) os="darwin" ;; | |
| Linux) os="linux" ;; | |
| *) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;; | |
| esac | |
| case "$(uname -m)" in | |
| x86_64|amd64) arch="x64" ;; | |
| arm64|aarch64) arch="arm64" ;; | |
| *) echo "Unsupported arch: $(uname -m)" >&2; exit 1 ;; | |
| esac | |
| # Rosetta 2 detection | |
| if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then | |
| if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then | |
| arch="arm64" | |
| fi | |
| fi | |
| # musl detection | |
| if [ "$os" = "linux" ]; then | |
| if [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || ldd /bin/ls 2>&1 | grep -q musl; then | |
| echo "linux-${arch}-musl" | |
| return | |
| fi | |
| fi | |
| echo "${os}-${arch}" | |
| } | |
| PLATFORM=$(detect_platform) | |
| # Fetch all remote versions from GCS bucket | |
| fetch_remote_versions() { | |
| local url="${GCS_API}?prefix=claude-code-releases/2.&delimiter=/&maxResults=500" | |
| local json | |
| json=$(curl -fsSL "$url") || { echo "Failed to fetch version list" >&2; return 1; } | |
| echo "$json" | python3 -c " | |
| import json, sys | |
| data = json.load(sys.stdin) | |
| for p in data.get('prefixes', []): | |
| print(p.replace('claude-code-releases/', '').rstrip('/')) | |
| " | sort -V | |
| } | |
| # List local versions | |
| list_local_versions() { | |
| if [ -d "$VERSIONS_DIR" ]; then | |
| for f in "$VERSIONS_DIR"/*; do | |
| [ -f "$f" ] && basename "$f" | |
| done | sort -V | |
| fi | |
| } | |
| # Get current active version | |
| get_active_version() { | |
| local claude_bin | |
| claude_bin=$(command -v claude 2>/dev/null) || return | |
| "$claude_bin" --version 2>/dev/null | head -1 | awk '{print $1}' | |
| } | |
| # Install a specific version | |
| install_version() { | |
| local version="$1" | |
| local dest="$VERSIONS_DIR/$version" | |
| if [ -f "$dest" ]; then | |
| echo -e "${YELLOW}Version $version is already installed.${RESET}" | |
| return 0 | |
| fi | |
| echo -e "${BLUE}Fetching manifest for $version...${RESET}" | |
| local manifest | |
| manifest=$(curl -fsSL "$GCS_BUCKET/$version/manifest.json") || { | |
| echo -e "${RED}Failed to fetch manifest. Version $version may not exist.${RESET}" >&2 | |
| return 1 | |
| } | |
| local checksum | |
| checksum=$(echo "$manifest" | python3 -c " | |
| import json, sys | |
| data = json.load(sys.stdin) | |
| p = data.get('platforms', {}).get('$PLATFORM', {}) | |
| print(p.get('checksum', '')) | |
| ") | |
| if [ -z "$checksum" ] || [[ ! "$checksum" =~ ^[a-f0-9]{64}$ ]]; then | |
| echo -e "${RED}Platform $PLATFORM not found in manifest for $version.${RESET}" >&2 | |
| return 1 | |
| fi | |
| local size | |
| size=$(echo "$manifest" | python3 -c " | |
| import json, sys | |
| data = json.load(sys.stdin) | |
| p = data.get('platforms', {}).get('$PLATFORM', {}) | |
| s = p.get('size', 0) | |
| print(f'{s / 1048576:.1f}MB' if s else 'unknown size') | |
| ") | |
| echo -e "${BLUE}Downloading claude $version ($size) for $PLATFORM...${RESET}" | |
| mkdir -p "$VERSIONS_DIR" | |
| local tmp="$VERSIONS_DIR/.claude-$version.tmp" | |
| if ! curl -fSL --progress-bar -o "$tmp" "$GCS_BUCKET/$version/$PLATFORM/claude"; then | |
| echo -e "${RED}Download failed.${RESET}" >&2 | |
| rm -f "$tmp" | |
| return 1 | |
| fi | |
| echo -e "${BLUE}Verifying checksum...${RESET}" | |
| local actual | |
| if [ "$(uname -s)" = "Darwin" ]; then | |
| actual=$(shasum -a 256 "$tmp" | cut -d' ' -f1) | |
| else | |
| actual=$(sha256sum "$tmp" | cut -d' ' -f1) | |
| fi | |
| if [ "$actual" != "$checksum" ]; then | |
| echo -e "${RED}Checksum mismatch!${RESET}" | |
| echo -e " expected: $checksum" | |
| echo -e " actual: $actual" | |
| rm -f "$tmp" | |
| return 1 | |
| fi | |
| mv "$tmp" "$dest" | |
| chmod +x "$dest" | |
| echo -e "${GREEN}✓ Installed claude $version${RESET}" | |
| } | |
| # Remove a local version | |
| remove_version() { | |
| local version="$1" | |
| local dest="$VERSIONS_DIR/$version" | |
| if [ ! -f "$dest" ]; then | |
| echo -e "${RED}Version $version is not installed.${RESET}" | |
| return 1 | |
| fi | |
| local active | |
| active=$(get_active_version) | |
| if [ "$version" = "$active" ]; then | |
| echo -e "${YELLOW}Warning: $version appears to be the active version.${RESET}" | |
| read -rp "Remove anyway? [y/N] " confirm | |
| [[ "$confirm" =~ ^[Yy]$ ]] || return 0 | |
| fi | |
| rm -f "$dest" | |
| echo -e "${GREEN}✓ Removed claude $version${RESET}" | |
| } | |
| # Display help | |
| usage() { | |
| cat <<EOF | |
| ${BOLD}Claude Code Version Manager${RESET} | |
| ${BOLD}Usage:${RESET} $(basename "$0") [command] [args] | |
| ${BOLD}Commands:${RESET} | |
| list List remote versions (latest 20) and local installs | |
| list --all List all remote versions | |
| install [VERSION] Install a version (interactive if no version given) | |
| remove [VERSION] Remove a local version | |
| help Show this help | |
| ${BOLD}Examples:${RESET} | |
| $(basename "$0") # Interactive mode | |
| $(basename "$0") list # Show versions | |
| $(basename "$0") install 2.1.77 # Install specific version | |
| ${DIM}Platform: $PLATFORM | Versions dir: $VERSIONS_DIR${RESET} | |
| EOF | |
| } | |
| # Interactive menu | |
| interactive_menu() { | |
| while true; do | |
| echo "" | |
| echo -e "${BOLD}Claude Code Version Manager${RESET} ${DIM}[$PLATFORM]${RESET}" | |
| echo -e "${DIM}─────────────────────────────────────────${RESET}" | |
| echo -e " ${BOLD}1)${RESET} List versions" | |
| echo -e " ${BOLD}2)${RESET} Install a version" | |
| echo -e " ${BOLD}3)${RESET} Remove a version" | |
| echo -e " ${BOLD}q)${RESET} Quit" | |
| echo "" | |
| read -rp "Choose [1-3/q]: " choice | |
| case "$choice" in | |
| 1) cmd_list false ;; | |
| 2) cmd_install_interactive ;; | |
| 3) cmd_remove_interactive ;; | |
| q|Q) echo "Bye."; exit 0 ;; | |
| *) echo -e "${RED}Invalid choice.${RESET}" ;; | |
| esac | |
| done | |
| } | |
| # List command | |
| cmd_list() { | |
| local show_all="${1:-false}" | |
| echo -e "\n${BLUE}Fetching remote versions...${RESET}" | |
| local remote_versions | |
| remote_versions=$(fetch_remote_versions) || return 1 | |
| local total | |
| total=$(echo "$remote_versions" | wc -l | tr -d ' ') | |
| local local_versions | |
| local_versions=$(list_local_versions) | |
| local active | |
| active=$(get_active_version) | |
| if [ "$show_all" = "false" ]; then | |
| remote_versions=$(echo "$remote_versions" | tail -20) | |
| echo -e "\n${BOLD}Available versions${RESET} ${DIM}(latest 20 of $total, use --all for full list)${RESET}\n" | |
| else | |
| echo -e "\n${BOLD}All available versions${RESET} ${DIM}($total total)${RESET}\n" | |
| fi | |
| while IFS= read -r v; do | |
| local marker=" " | |
| local color="" | |
| local suffix="" | |
| if echo "$local_versions" | grep -qx "$v"; then | |
| marker="${GREEN}●${RESET} " | |
| suffix=" ${DIM}[installed]${RESET}" | |
| fi | |
| if [ "$v" = "$active" ]; then | |
| marker="${CYAN}▸${RESET} " | |
| suffix=" ${CYAN}[active]${RESET}" | |
| fi | |
| echo -e " ${marker}${v}${suffix}" | |
| done <<< "$remote_versions" | |
| if [ -n "$local_versions" ]; then | |
| echo "" | |
| echo -e "${DIM}● = installed ▸ = active${RESET}" | |
| fi | |
| } | |
| # Interactive install | |
| cmd_install_interactive() { | |
| echo -e "\n${BLUE}Fetching remote versions...${RESET}" | |
| local remote_versions | |
| remote_versions=$(fetch_remote_versions) || return 1 | |
| local local_versions | |
| local_versions=$(list_local_versions) | |
| # Show last 20 versions with numbers | |
| local versions_arr=() | |
| while IFS= read -r v; do | |
| versions_arr+=("$v") | |
| done <<< "$(echo "$remote_versions" | tail -20)" | |
| echo -e "\n${BOLD}Select a version to install:${RESET}\n" | |
| for i in "${!versions_arr[@]}"; do | |
| local v="${versions_arr[$i]}" | |
| local suffix="" | |
| if echo "$local_versions" | grep -qx "$v"; then | |
| suffix=" ${DIM}[installed]${RESET}" | |
| fi | |
| printf " ${BOLD}%2d)${RESET} %s%b\n" $((i + 1)) "$v" "$suffix" | |
| done | |
| echo "" | |
| read -rp "Enter number or version string (q to cancel): " input | |
| [[ "$input" =~ ^[Qq]$ ]] && return 0 | |
| local target | |
| if [[ "$input" =~ ^[0-9]+$ ]] && [ "$input" -ge 1 ] && [ "$input" -le "${#versions_arr[@]}" ]; then | |
| target="${versions_arr[$((input - 1))]}" | |
| elif [[ "$input" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| target="$input" | |
| else | |
| echo -e "${RED}Invalid selection.${RESET}" | |
| return 1 | |
| fi | |
| install_version "$target" | |
| } | |
| # Interactive remove | |
| cmd_remove_interactive() { | |
| local local_versions | |
| local_versions=$(list_local_versions) | |
| if [ -z "$local_versions" ]; then | |
| echo -e "\n${YELLOW}No local versions installed.${RESET}" | |
| return 0 | |
| fi | |
| local versions_arr=() | |
| while IFS= read -r v; do | |
| versions_arr+=("$v") | |
| done <<< "$local_versions" | |
| local active | |
| active=$(get_active_version) | |
| echo -e "\n${BOLD}Installed versions:${RESET}\n" | |
| for i in "${!versions_arr[@]}"; do | |
| local v="${versions_arr[$i]}" | |
| local suffix="" | |
| if [ "$v" = "$active" ]; then | |
| suffix=" ${CYAN}[active]${RESET}" | |
| fi | |
| local size | |
| size=$(du -h "$VERSIONS_DIR/$v" 2>/dev/null | awk '{print $1}') | |
| printf " ${BOLD}%2d)${RESET} %s ${DIM}%s${RESET}%b\n" $((i + 1)) "$v" "$size" "$suffix" | |
| done | |
| echo "" | |
| read -rp "Enter number to remove (q to cancel): " input | |
| [[ "$input" =~ ^[Qq]$ ]] && return 0 | |
| if [[ "$input" =~ ^[0-9]+$ ]] && [ "$input" -ge 1 ] && [ "$input" -le "${#versions_arr[@]}" ]; then | |
| remove_version "${versions_arr[$((input - 1))]}" | |
| else | |
| echo -e "${RED}Invalid selection.${RESET}" | |
| return 1 | |
| fi | |
| } | |
| # Main | |
| case "${1:-}" in | |
| list) | |
| [[ "${2:-}" = "--all" ]] && cmd_list true || cmd_list false | |
| ;; | |
| install) | |
| if [ -n "${2:-}" ]; then | |
| install_version "$2" | |
| else | |
| cmd_install_interactive | |
| fi | |
| ;; | |
| remove) | |
| if [ -n "${2:-}" ]; then | |
| remove_version "$2" | |
| else | |
| cmd_remove_interactive | |
| fi | |
| ;; | |
| help|--help|-h) | |
| usage | |
| ;; | |
| "") | |
| interactive_menu | |
| ;; | |
| *) | |
| echo -e "${RED}Unknown command: $1${RESET}" >&2 | |
| usage | |
| exit 1 | |
| ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment