#!/usr/bin/env bash # Rabijinnie's Space:直接下载发布程序,不调用 claude.ai/install.sh。 set -euo pipefail usage() { printf '%s\n' '用法:bash cc.sh [latest|stable|版本号] [--dry-run]' \ '默认 latest;推荐命令显式传入 2.1.153。--dry-run 不联网、不安装。' } fail() { printf '错误:%s\n' "$*" >&2; exit 1; } target=latest target_seen=false dry_run=false for arg in "$@"; do case "$arg" in -h|--help) usage; exit 0 ;; --dry-run) dry_run=true ;; *) if [ "$target_seen" = true ] || [[ ! "$arg" =~ ^(latest|stable|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then usage >&2; exit 2 fi target="$arg"; target_seen=true ;; esac done case "$(uname -s)" in Darwin) os=darwin ;; Linux) os=linux ;; *) fail '此脚本用于 macOS / Linux / WSL。Windows 请使用 cc.ps1。' ;; esac case "$(uname -m)" in x86_64|amd64) arch=x64 ;; arm64|aarch64) arch=arm64 ;; *) fail '仅支持 x64 或 ARM64 处理器。' ;; esac if [ "$os" = darwin ] && [ "$arch" = x64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then arch=arm64 fi platform="$os-$arch" if [ "$os" = linux ]; then if [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || \ (ldd --version 2>&1 || true) | grep -qi musl; then platform="$platform-musl" fi fi repo='https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases' printf 'Claude Code 直接安装\n所选版本:%s\n系统架构:%s\n程序源:%s\n' "$target" "$platform" "$repo" printf '%s\n' '流程:读取对应版本清单 → 下载程序 → 校验 SHA256 和大小 → 安装。' if [ "$dry_run" = true ]; then exit 0; fi command -v curl >/dev/null 2>&1 || fail '请先安装 curl,再重新运行。' if command -v sha256sum >/dev/null 2>&1; then hash_tool=sha256sum elif command -v shasum >/dev/null 2>&1; then hash_tool=shasum else fail '需要 sha256sum 或 shasum 才能校验下载文件。' fi stage_dir="$(mktemp -d "${TMPDIR:-/tmp}/claude-code-setup.XXXXXXXX")" version_tmp='' link_stage='' cleanup() { rm -rf -- "$stage_dir" if [ -n "$version_tmp" ]; then rm -f -- "$version_tmp"; fi if [ -n "$link_stage" ]; then rm -rf -- "$link_stage"; fi } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM download() { local url="$1" destination="$2" limit="$3" if ! curl --fail --location --show-error --proto '=https' --proto-redir '=https' \ --connect-timeout 20 --max-time "$limit" --retry 2 --retry-delay 2 \ --output "$destination" "$url"; then fail '下载失败,已停止。请检查程序源的连接、版本是否存在以及网络设置;不会自动换成其他版本。' fi [ -s "$destination" ] || fail '下载内容为空,已停止。' } reject_webpage() { if LC_ALL=C grep -qiE '^[[:space:]]*<(!doctype[[:space:]]+html|html([[:space:]>]))' "$1"; then fail '程序源返回了 HTML 网页,已停止。请检查网络认证、代理或访问限制。' fi } # 仅提取发布清单中的扁平平台对象;字段经过格式和版本检查后才使用。 json_string() { printf '%s' "$1" | sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p" } printf '\n[1/4] 读取版本与校验清单……\n' version="$target" case "$target" in latest|stable) download "$repo/$target" "$stage_dir/channel" 120 reject_webpage "$stage_dir/channel" version="$(tr -d '\r\n' < "$stage_dir/channel")" ;; esac [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail '程序源返回了无效版本号,已停止。' printf '实际下载版本:%s\n' "$version" download "$repo/$version/manifest.json" "$stage_dir/manifest.json" 120 reject_webpage "$stage_dir/manifest.json" compact="$(tr -d '\r\n\t ' < "$stage_dir/manifest.json")" [ "$(json_string "$compact" version)" = "$version" ] || fail '清单版本与所选版本不一致,已停止。' entry="$(printf '%s' "$compact" | sed -n "s/.*\"$platform\":\({[^{}]*}\).*/\1/p")" [ -n "$entry" ] || fail "这个版本没有 ${platform} 的安装文件。" checksum="$(json_string "$entry" checksum)" [[ "$checksum" =~ ^[a-fA-F0-9]{64}$ ]] || fail '清单中的 SHA256 格式无效,已停止。' checksum="$(printf '%s' "$checksum" | tr 'A-F' 'a-f')" expected_size="$(printf '%s' "$entry" | sed -n 's/.*"size":\([0-9][0-9]*\)[,}].*/\1/p')" [[ "$expected_size" =~ ^[1-9][0-9]*$ ]] || fail '清单中的文件大小无效,已停止。' [ "$(json_string "$entry" binary)" = claude ] || fail '清单中的程序名称无效,已停止。' printf '\n[2/4] 下载程序(约 %s 字节,耗时取决于网络)……\n' "$expected_size" printf '下载地址:%s/%s/%s/claude\n' "$repo" "$version" "$platform" binary_path="$stage_dir/claude" download "$repo/$version/$platform/claude" "$binary_path" 1800 actual_size="$(wc -c < "$binary_path" | tr -d '[:space:]')" [ "$actual_size" = "$expected_size" ] || fail "文件大小不匹配(应为 ${expected_size},实际 ${actual_size}),未安装。" if [ "$hash_tool" = sha256sum ]; then actual_hash="$(sha256sum "$binary_path" | cut -d' ' -f1)" else actual_hash="$(shasum -a 256 "$binary_path" | cut -d' ' -f1)" fi [ "$actual_hash" = "$checksum" ] || fail 'SHA256 校验失败,未运行程序、未替换已有安装。' printf '%s\n' 'SHA256 与文件大小校验通过。' printf '\n[3/4] 检查程序并安装……\n' chmod +x "$binary_path" if ! version_output="$("$binary_path" --version)"; then fail '下载的程序无法启动,未替换已有安装。请检查系统版本及运行依赖。' fi [[ "$version_output" =~ ^([0-9]+\.[0-9]+\.[0-9]+)[[:space:]]+\(Claude\ Code\) ]] || fail '程序没有返回预期版本信息,未安装。' [ "${BASH_REMATCH[1]}" = "$version" ] || fail '程序实际版本与清单不一致,未安装。' versions_dir="$HOME/.local/share/claude/versions" bin_dir="$HOME/.local/bin" link_path="$bin_dir/claude" mkdir -p "$versions_dir" "$bin_dir" [ ! -d "$versions_dir/$version" ] || fail '版本文件位置是一个目录,已停止,请检查现有安装。' [ ! -d "$link_path" ] || fail 'claude 命令位置是一个目录,已停止,请检查现有安装。' version_tmp="$(mktemp "$versions_dir/.claude-install.XXXXXXXX")" cp "$binary_path" "$version_tmp" chmod +x "$version_tmp" mv -f "$version_tmp" "$versions_dir/$version" version_tmp='' link_stage="$(mktemp -d "$bin_dir/.claude-link.XXXXXXXX")" ln -s "$versions_dir/$version" "$link_stage/claude" if [ -e "$link_path" ] || [ -L "$link_path" ]; then backup_path="$bin_dir/claude.backup.$(date +%Y%m%d%H%M%S).$$" cp -P "$link_path" "$backup_path" printf '原命令已备份:%s\n' "$backup_path" fi mv -f "$link_stage/claude" "$link_path" printf '\n[4/4] 安装完成\n%s\n位置:%s\n' "$version_output" "$link_path" printf '%s\n' '已有账号与 API 配置已保留。本脚本不修改自动更新设置;指定版本不等于永久锁定版本。' \ '打开新终端,输入 claude 就能启动。若提示找不到命令,先运行:' \ 'export PATH="$HOME/.local/bin:$PATH"' \ '再运行 claude --version 和 claude doctor。永久 PATH 配置见教程。'