#!/bin/bash # ============================================================ # Bookworm Portable - macOS Setup (从 boot 仓库内运行) # Version: 1.5 # # 用法: cd ~/bookworm-boot && bash Bookworm-Setup.sh # # 前提: 已 git clone bookworm-boot 到本地 # 功能: 检查依赖 → 代理检测 → 克隆配置 → 解密凭证 → 配置别名 # ============================================================ set -e # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' BOLD='\033[1m' # 配置 BOOT_DIR="$(cd "$(dirname "$0")" && pwd)" CLAUDE_DIR="$HOME/.claude" CONFIG_REPO="https://code.letcareme.com/bookworm/bookworm-config.git" SECRETS_ENC="$BOOT_DIR/secrets.enc" TOTAL_STEPS=6 banner() { echo "" echo -e "${CYAN} ____ _" echo " | __ ) ___ ___ | | ____ _____ _ __ _ __ ___" echo " | _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__| '\` _ \\" echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |" echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|" echo "" echo -e " ${BOLD}Portable macOS Setup v1.5${NC}" echo -e " ${BLUE}92 Skills | 18 Agents | 34 Hooks${NC}" echo -e "${NC}" } info() { echo -e " ${BLUE}[INFO]${NC} $1"; } success() { echo -e " ${GREEN}[OK]${NC} $1"; } warn() { echo -e " ${YELLOW}[!]${NC} $1"; } fail() { echo -e " ${RED}[!!]${NC} $1"; } step() { echo -e "\n${BOLD} [$1/$TOTAL_STEPS]${NC} ${CYAN}$2${NC}"; } # ============================================================ # Banner # ============================================================ banner # ============================================================ # Step 1: 检查并安装依赖 # ============================================================ step 1 "检查依赖软件" # Homebrew if ! command -v brew &>/dev/null; then warn "Homebrew 未安装, 正在安装 (可能需要输入系统密码)..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" if [ -f /opt/homebrew/bin/brew ]; then eval "$(/opt/homebrew/bin/brew shellenv)" PROFILE="$HOME/.zprofile" if ! grep -q 'homebrew' "$PROFILE" 2>/dev/null; then echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$PROFILE" fi fi success "Homebrew 安装完成" else success "Homebrew $(brew --version | head -1 | awk '{print $2}')" fi # Node.js if ! command -v node &>/dev/null; then info "通过 Homebrew 安装 Node.js..." brew install node success "Node.js $(node -v) 安装完成" else success "Node.js $(node -v)" fi # Git if ! command -v git &>/dev/null; then info "通过 Homebrew 安装 Git..." brew install git success "Git $(git --version | awk '{print $3}') 安装完成" else success "Git $(git --version | awk '{print $3}')" fi # OpenSSL OPENSSL_CMD="" for p in /opt/homebrew/opt/openssl/bin/openssl /usr/local/opt/openssl/bin/openssl openssl; do if command -v "$p" &>/dev/null; then OPENSSL_CMD="$p" break fi done if [ -z "$OPENSSL_CMD" ]; then info "通过 Homebrew 安装 OpenSSL..." brew install openssl OPENSSL_CMD="/opt/homebrew/opt/openssl/bin/openssl" success "OpenSSL 安装完成" else success "OpenSSL: $($OPENSSL_CMD version 2>/dev/null | head -1)" fi # Claude Code if ! command -v claude &>/dev/null; then info "通过 npm 安装 Claude Code..." npm i -g @anthropic-ai/claude-code success "Claude Code 安装完成" else success "Claude Code $(claude --version 2>/dev/null || echo 'installed')" fi # ============================================================ # Step 2: 检测代理 # ============================================================ step 2 "检测网络代理" export NO_PROXY="bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1" export no_proxy="$NO_PROXY" PROXY_FOUND="" # 环境变量 if [ -n "$HTTPS_PROXY" ] || [ -n "$https_proxy" ]; then PROXY_FOUND="${HTTPS_PROXY:-$https_proxy}" success "环境变量代理: $PROXY_FOUND" fi # macOS 系统代理 if [ -z "$PROXY_FOUND" ]; then PROXY_HOST=$(scutil --proxy 2>/dev/null | grep "HTTPSProxy" | awk '{print $3}') PROXY_PORT=$(scutil --proxy 2>/dev/null | grep "HTTPSPort" | awk '{print $3}') if [ -n "$PROXY_HOST" ] && [ "$PROXY_HOST" != "0" ] && [ -n "$PROXY_PORT" ] && [ "$PROXY_PORT" != "0" ]; then PROXY_FOUND="http://$PROXY_HOST:$PROXY_PORT" export HTTPS_PROXY="$PROXY_FOUND" export HTTP_PROXY="$PROXY_FOUND" success "macOS 系统代理: $PROXY_FOUND" fi fi # 常见端口扫描 (500ms 超时) if [ -z "$PROXY_FOUND" ]; then for PORT in 7890 7893 7891 1087 1080 8118; do if nc -z -w1 127.0.0.1 $PORT 2>/dev/null; then PROXY_FOUND="http://127.0.0.1:$PORT" export HTTPS_PROXY="$PROXY_FOUND" export HTTP_PROXY="$PROXY_FOUND" success "本地代理端口: $PORT" break fi done fi if [ -z "$PROXY_FOUND" ]; then warn "未检测到代理。在国内 Claude Code 可能无法启动。" warn "请启动代理软件 (ClashX / Surge / V2Ray) 后重试。" echo "" read -p " 无代理继续? (y/n): " CONTINUE if [ "$CONTINUE" != "y" ]; then exit 1; fi fi success "NO_PROXY: bww.letcareme.com,code.letcareme.com" # ============================================================ # Step 3: 克隆/更新配置仓库到 ~/.claude # ============================================================ step 3 "同步 Bookworm 配置" git config --global credential.helper osxkeychain 2>/dev/null || true if [ -d "$CLAUDE_DIR/.git" ]; then info "配置仓库已存在, 更新..." cd "$CLAUDE_DIR" && git pull --ff-only 2>/dev/null || git pull 2>/dev/null || true cd "$BOOT_DIR" success "配置仓库已更新" elif [ -f "$CLAUDE_DIR/CLAUDE.md" ]; then warn "~/.claude 已存在但非 git 仓库, 备份后克隆..." mv "$CLAUDE_DIR" "$CLAUDE_DIR.bak.$(date +%s)" git clone --depth 1 "$CONFIG_REPO" "$CLAUDE_DIR" success "配置仓库克隆完成 (旧目录已备份)" else info "首次安装, 克隆配置仓库 (需输入 Gitea 密码)..." mkdir -p "$(dirname "$CLAUDE_DIR")" git clone --depth 1 "$CONFIG_REPO" "$CLAUDE_DIR" success "配置仓库克隆完成" fi # 创建本地运行时目录 for d in debug sessions cache backups telemetry memory projects; do mkdir -p "$CLAUDE_DIR/$d" 2>/dev/null done # ============================================================ # Step 4: 解密凭证 (含 Keychain 本日免密) # ============================================================ step 4 "解密凭证" # Keychain 缓存相关 KEYCHAIN_SERVICE="bookworm-secrets" KEYCHAIN_ACCOUNT="$(whoami)" CACHE_LOADED=false # 尝试从 Keychain 加载缓存 load_cached_secrets() { local cached cached=$(security find-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w 2>/dev/null) || return 1 # 检查是否过期 (缓存格式: EXPIRY=ISO日期\nKEY=VALUE\n...) local expiry expiry=$(echo "$cached" | head -1) local expiry_date="${expiry#EXPIRY=}" local today today=$(date +%Y-%m-%d) if [ "$expiry_date" != "$today" ]; then # 已过期,删除缓存 security delete-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" 2>/dev/null || true return 1 fi # 加载环境变量 (跳过 EXPIRY 行) local count=0 while IFS= read -r line; do [ -z "$line" ] && continue [[ "$line" == EXPIRY=* ]] && continue local key="${line%%=*}" local value="${line#*=}" key=$(echo "$key" | tr -d ' ') if [ -n "$key" ] && [ -n "$value" ]; then export "$key=$value" count=$((count + 1)) fi done <<< "$cached" if [ $count -gt 0 ] && [ -n "$ANTHROPIC_API_KEY" ]; then success "从 Keychain 缓存加载 $count 个凭证 (免密)" CACHE_LOADED=true return 0 fi return 1 } # 保存凭证到 Keychain save_secrets_to_cache() { local today today=$(date +%Y-%m-%d) local data="EXPIRY=$today" local env_keys="ANTHROPIC_API_KEY ANTHROPIC_BASE_URL GITHUB_PERSONAL_ACCESS_TOKEN SLACK_BOT_TOKEN ATLASSIAN_API_TOKEN BROWSERBASE_API_KEY FIRECRAWL_API_KEY" for k in $env_keys; do local v="${!k}" if [ -n "$v" ]; then data="$data $k=$v" fi done security add-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w "$data" -U 2>/dev/null && \ success "凭证已缓存至今日 23:59 (下次免密)" || \ warn "Keychain 缓存失败 (不影响使用)" } # 解密工具: 优先 node crypto-helper.js (BWENC1 格式), 回退 openssl CRYPTO_HELPER="$BOOT_DIR/crypto-helper.js" _decrypt_secrets() { local pass="$1" enc="$2" if command -v node &>/dev/null && [ -f "$CRYPTO_HELPER" ]; then node "$CRYPTO_HELPER" decrypt "$pass" "$enc" 2>/dev/null elif [ -n "$OPENSSL_CMD" ]; then $OPENSSL_CMD enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in "$enc" -pass pass:"$pass" 2>/dev/null else return 1 fi } # 先尝试缓存 if load_cached_secrets 2>/dev/null; then : # 缓存加载成功 elif [ -f "$SECRETS_ENC" ]; then DECRYPTED="" for attempt in 1 2 3; do echo "" read -rs -p " 输入主密码解密凭证 (第 $attempt/3 次): " PASSWORD echo "" DECRYPTED=$(_decrypt_secrets "$PASSWORD" "$SECRETS_ENC") || true PASSWORD="" if [ -n "$DECRYPTED" ]; then while IFS= read -r line; do [ -z "$line" ] && continue key="${line%%=*}" value="${line#*=}" key=$(echo "$key" | tr -d ' ') if [ -n "$key" ] && [ -n "$value" ]; then export "$key=$value" success "已注入: $key" fi done <<< "$DECRYPTED" DECRYPTED="" # 询问是否缓存 echo "" read -p " 今日内免密启动? (y/n): " CACHE_CHOICE if [ "$CACHE_CHOICE" = "y" ] || [ "$CACHE_CHOICE" = "Y" ]; then save_secrets_to_cache fi break else if [ $attempt -lt 3 ]; then warn "密码错误, 剩余重试: $((3 - attempt)) 次" else fail "3 次密码均错误, 凭证未解密" warn "可稍后手动配置 API Key" fi fi done else if [ ! -f "$SECRETS_ENC" ]; then warn "secrets.enc 不存在, 跳过凭证解密" info "请联系管理员获取加密凭证文件" fi fi # 渲染 settings.json (替换占位符) TEMPLATE_FILE="$CLAUDE_DIR/settings.template.json" SETTINGS_FILE="$CLAUDE_DIR/settings.json" if [ -f "$TEMPLATE_FILE" ]; then CLAUDE_ROOT=$(echo "$CLAUDE_DIR" | sed 's/\\/\//g') sed "s|{{CLAUDE_ROOT}}|$CLAUDE_ROOT|g; s|{{HOME}}|$HOME|g" "$TEMPLATE_FILE" > "$SETTINGS_FILE" success "settings.json 已渲染" fi # ============================================================ # Step 5: 配置终端别名 # ============================================================ step 5 "配置终端快捷命令" SHELL_RC="$HOME/.zshrc" if [ -n "$BASH_VERSION" ] && [ -f "$HOME/.bashrc" ]; then SHELL_RC="$HOME/.bashrc" fi ALIAS_MARKER="# Bookworm Portable aliases" if ! grep -q "$ALIAS_MARKER" "$SHELL_RC" 2>/dev/null; then cat >> "$SHELL_RC" << 'ALIASES' # Bookworm Portable aliases alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" claude' alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"' ALIASES success "已添加到 $SHELL_RC:" info " bw -- 启动 Bookworm" info " bw-update -- 更新 Bookworm" else # 更新旧别名 (bookworm → bw) if grep -q "alias bookworm=" "$SHELL_RC" 2>/dev/null; then # 删除旧别名块,然后追加新的 sed -i '' '/# Bookworm Portable aliases/,/^$/d' "$SHELL_RC" 2>/dev/null || true cat >> "$SHELL_RC" << 'ALIASES' # Bookworm Portable aliases alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" claude' alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"' ALIASES success "终端别名已更新 (bookworm → bw)" else success "终端别名已配置" fi fi # ============================================================ # Step 6: 完成 # ============================================================ step 6 "安装完成" echo "" echo -e "${GREEN} ============================================================${NC}" echo -e "${GREEN} Bookworm Smart Assistant for macOS 安装完成!${NC}" echo -e "${GREEN} ============================================================${NC}" echo "" echo -e " 已安装:" echo -e " ${GREEN}[v]${NC} Homebrew ${GREEN}[v]${NC} Node.js $(node -v 2>/dev/null)" echo -e " ${GREEN}[v]${NC} Git ${GREEN}[v]${NC} OpenSSL" echo -e " ${GREEN}[v]${NC} Claude Code ${GREEN}[v]${NC} Bookworm (92 Skills)" echo "" echo -e " ${BOLD}启动方式:${NC}" echo -e " 终端输入: ${CYAN}bw${NC}" echo "" echo -e " ${BOLD}更新:${NC}" echo -e " 终端输入: ${CYAN}bw-update${NC}" echo "" # 询问是否立即启动 read -p " 立即启动 Bookworm? (y/n): " START_NOW if [ "$START_NOW" = "y" ] || [ "$START_NOW" = "Y" ]; then echo "" info "正在启动 Claude Code..." cd "$HOME" export NO_PROXY="bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1" exec claude fi