- Bookworm-Setup.sh: macOS Keychain 本日免密缓存(对标 Windows Credential Manager) - Bookworm-OneClick-Mac.sh: bw-update 补 config 仓库更新 - sync-version.js: 新建版本号同步脚本(从 stats-compiled.json 自动注入) - quick-start.html/quick-reference.txt: Hooks 29→34 修正 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
389 lines
12 KiB
Bash
389 lines
12 KiB
Bash
#!/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 缓存失败 (不影响使用)"
|
|
}
|
|
|
|
# 先尝试缓存
|
|
if load_cached_secrets 2>/dev/null; then
|
|
: # 缓存加载成功
|
|
elif [ -f "$SECRETS_ENC" ] && [ -n "$OPENSSL_CMD" ]; then
|
|
DECRYPTED=""
|
|
for attempt in 1 2 3; do
|
|
echo ""
|
|
read -rs -p " 输入主密码解密凭证 (第 $attempt/3 次): " PASSWORD
|
|
echo ""
|
|
DECRYPTED=$($OPENSSL_CMD enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in "$SECRETS_ENC" -pass pass:"$PASSWORD" 2>/dev/null) || 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
|