commit 5c005e88500241e98f1207e5b261a286a7891761 Author: leesu Date: Wed Apr 1 12:47:44 2026 +0800 Bookworm Portable boot scripts v1.1 diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..44e4e1e --- /dev/null +++ b/README.txt @@ -0,0 +1,51 @@ +Bookworm Portable v1.1 - 纯云端便携部署工具包 +================================================ + +=== 文件说明 === + + deploy-gitea.sh ECS Gitea 部署 (服务端,执行一次) + prepare-repo.ps1 仓库准备 (本机执行一次) + encrypt-secrets.ps1 凭证加密 (本机执行一次) + settings.template.json settings.json 模板 + settings.local.template.json settings.local.json 模板 (权限白名单) + install.ps1 安装/启动 (目标机执行) + stop.ps1 清理/卸载 (目标机执行) + +=== 一次性部署 === + + 步骤 1: 部署 Gitea (ECS) + > scp deploy-gitea.sh root@8.138.11.105:/tmp/ + > ssh root@8.138.11.105 "GITEA_ADMIN_PASS='你的密码' bash /tmp/deploy-gitea.sh" + > 登录 http://8.138.11.105:3000 创建两个私有仓库: + - bookworm-config (系统文件) + - bookworm-boot (引导脚本+加密凭证) + + 步骤 2: 推送 Bookworm 配置 + > .\prepare-repo.ps1 -GitUrl "http://8.138.11.105:3000/leesu/bookworm-config.git" + + 步骤 3: 加密凭证 + > .\encrypt-secrets.ps1 + > (输入中转站 API Key + MCP 凭证 + 设置主密码,至少 12 位) + + 步骤 4: 推送 boot 仓库 + > 将 install.ps1, stop.ps1, secrets.enc 推送到 bookworm-boot 仓库 + +=== 目标机使用 === + + 安装: .\install.ps1 + 清理: .\stop.ps1 + 恢复: .\stop.ps1 -Restore + 深度: .\stop.ps1 -Deep + +=== 目标机要求 === + + [必须] Claude Code, Node.js >= 18, Git + [可选] Python 3.x, openssl (Git for Windows 自带) + +=== 安全规格 === + + 加密: AES-256-CBC + PBKDF2 (600000 迭代, OWASP 2023) + 凭证: 仅进程级环境变量,不写磁盘/注册表 + Gitea: INSTALL_LOCK=true, 注册关闭, 管理员 CLI 创建 + 密码: openssl stdin 管道传入,不暴露在进程列表 + 校验: Gitea 二进制 SHA256 完整性校验 diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..277f924 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,302 @@ +<# +.SYNOPSIS + Bookworm Portable - 安装/启动脚本 +.DESCRIPTION + 从 Gitea 私有仓克隆 Bookworm 配置到目标机, + 解密凭证, 渲染模板, 启动 Claude Code. +.USAGE + # 首次安装 (从 Gitea 克隆) + .\install.ps1 + + # 仅启动 (已安装过) + .\install.ps1 -StartOnly + + # 指定 Gitea 地址 + .\install.ps1 -GitUrl "http://8.138.11.105:3000/leesu/bookworm-config.git" +#> + +param( + [string]$GitUrl = "https://code.letcareme.com/leesu/bookworm-config.git", + [switch]$StartOnly, + [switch]$SkipSecrets +) + +$ErrorActionPreference = "Stop" + +# ─── 路径定义 ──────────────────────────────────────── +$ScriptDir = if ($MyInvocation.MyCommand.Path) { + Split-Path -Parent $MyInvocation.MyCommand.Path +} else { $PWD.Path } +$ClaudeTarget = Join-Path $env:USERPROFILE ".claude" +$BackupPath = Join-Path $env:USERPROFILE ".claude.bw-backup" +$SecretsEnc = Join-Path $ScriptDir "secrets.enc" +$TemplateFile = Join-Path $ClaudeTarget "settings.template.json" +$LocalTplFile = Join-Path $ClaudeTarget "settings.local.template.json" +$SettingsFile = Join-Path $ClaudeTarget "settings.json" +$LocalSetFile = Join-Path $ClaudeTarget "settings.local.json" +$DebugDir = Join-Path $ClaudeTarget "debug" + +# ─── openssl 检测 ──────────────────────────────────── +$opensslCmd = (Get-Command openssl -ErrorAction SilentlyContinue)?.Source +if (-not $opensslCmd) { + $gitOpenssl = "C:\Program Files\Git\usr\bin\openssl.exe" + if (Test-Path $gitOpenssl) { $opensslCmd = $gitOpenssl } +} + +# ─── 辅助函数 ──────────────────────────────────────── + +function Write-Banner { + Write-Host "" + Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Cyan + Write-Host " ║ Bookworm Portable Installer v1.1 ║" -ForegroundColor Cyan + Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Cyan + Write-Host "" +} + +function Test-Command($cmd) { + return [bool](Get-Command $cmd -ErrorAction SilentlyContinue) +} + +function Decrypt-Secrets { + if ($SkipSecrets -or -not (Test-Path $SecretsEnc)) { + Write-Host " [!] 跳过凭证解密 (无 secrets.enc)" -ForegroundColor Yellow + return + } + if (-not $opensslCmd) { + Write-Host " [!] openssl 未找到,跳过凭证解密" -ForegroundColor Yellow + return + } + + $password = Read-Host " 输入主密码解密凭证" -AsSecureString + $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) + $plainPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) + + try { + # 通过 stdin 传入密码,避免进程列表泄露 + $decrypted = $plainPwd | & $opensslCmd enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in $SecretsEnc -pass stdin 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "解密失败,密码错误?" + } + + # 使用 IndexOf 正确分割 key=value (value 中可能含 = 号) + $decrypted -split "`n" | ForEach-Object { + $line = $_.Trim() + if ($line -and $line.Contains('=')) { + $eqIdx = $line.IndexOf('=') + $key = $line.Substring(0, $eqIdx).Trim() + $val = $line.Substring($eqIdx + 1).Trim() + [System.Environment]::SetEnvironmentVariable($key, $val, "Process") + Write-Host " [OK] 已注入: $key" -ForegroundColor Green + } + } + } + catch { + Write-Host " [ERROR] 凭证解密失败: $_" -ForegroundColor Red + # 检查关键凭证是否存在 + if (-not $env:ANTHROPIC_API_KEY) { + Write-Host " [WARN] ANTHROPIC_API_KEY 未设置,Claude Code 可能无法使用中转站" -ForegroundColor Yellow + $continue = Read-Host " 继续启动? (y/n)" + if ($continue -ne 'y') { exit 1 } + } + } + finally { + $plainPwd = $null + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) + } +} + +function Render-SettingsTemplate { + if (-not (Test-Path $TemplateFile)) { + Write-Host " [!] 未找到 settings.template.json,跳过渲染" -ForegroundColor Yellow + return + } + + $claudeRoot = $ClaudeTarget.Replace('\', '/') + # HOME 保留反斜杠格式,与 Claude Code 原始行为一致 + $homeDir = $env:USERPROFILE + + $content = Get-Content $TemplateFile -Raw + $content = $content -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot + $content = $content -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\') + + Set-Content $SettingsFile -Value $content -Encoding UTF8 + Write-Host " [OK] settings.json 已渲染 (ROOT=$claudeRoot)" -ForegroundColor Green + + # 渲染 settings.local.template.json (如果存在) + if (Test-Path $LocalTplFile) { + $localContent = Get-Content $LocalTplFile -Raw + $localContent = $localContent -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot + $localContent = $localContent -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\') + $localContent = $localContent -replace '\{\{USERNAME\}\}', $env:USERNAME + Set-Content $LocalSetFile -Value $localContent -Encoding UTF8 + Write-Host " [OK] settings.local.json 已渲染" -ForegroundColor Green + } +} + +# ─── 主流程 ────────────────────────────────────────── + +Write-Banner + +# 步骤 0: 前置检查 +Write-Host "[0/5] 前置检查..." -ForegroundColor White +$checks = @( + @{ Name = "Claude Code"; OK = (Test-Command "claude") } + @{ Name = "Node.js"; OK = (Test-Command "node") } + @{ Name = "Git"; OK = (Test-Command "git") } +) +foreach ($c in $checks) { + $icon = if ($c.OK) { "[OK]" } else { "[!!]" } + $color = if ($c.OK) { "Green" } else { "Red" } + Write-Host " $icon $($c.Name)" -ForegroundColor $color +} +if (-not (Test-Command "claude")) { + Write-Host "`n [ABORT] Claude Code 未安装,请先安装后重试" -ForegroundColor Red + exit 1 +} +if (-not (Test-Command "node")) { + Write-Host "`n [ABORT] Node.js 未安装,Bookworm Hooks 无法运行" -ForegroundColor Red + exit 1 +} + +# 步骤 1: 解密凭证 (进程级环境变量,不写磁盘) +Write-Host "`n[1/6] 解密凭证..." -ForegroundColor White +Decrypt-Secrets + +# 步骤 2: 克隆/更新仓库 +if (-not $StartOnly) { + Write-Host "`n[2/6] 同步 Bookworm 配置..." -ForegroundColor White + + if (Test-Path $ClaudeTarget) { + $isGit = Test-Path (Join-Path $ClaudeTarget ".git") + if ($isGit) { + Write-Host " 已有仓库,执行 git pull..." + Push-Location $ClaudeTarget + try { + # stash 本地修改,防止 rebase 冲突 + $stashOutput = git stash 2>&1 + $hasStash = $stashOutput -notmatch "No local changes" + git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" } + if ($hasStash) { + git stash pop 2>&1 | ForEach-Object { Write-Host " $_" } + } + } + catch { + Write-Host " [WARN] git pull 失败: $_" -ForegroundColor Yellow + Write-Host " 使用本地现有版本继续" -ForegroundColor Yellow + } + finally { Pop-Location } + } + else { + # 安全克隆: 先克隆到临时目录,成功后再替换 + Write-Host " 备份现有 .claude/ 并克隆..." + $tempClone = "$ClaudeTarget.bw-clone-temp" + try { + if (Test-Path $tempClone) { Remove-Item $tempClone -Recurse -Force } + git clone --depth 1 $GitUrl $tempClone 2>&1 | ForEach-Object { Write-Host " $_" } + + # 克隆成功,执行替换 + if (Test-Path $BackupPath) { Remove-Item $BackupPath -Recurse -Force } + Rename-Item $ClaudeTarget $BackupPath + Rename-Item $tempClone $ClaudeTarget + Write-Host " [OK] 克隆完成,原始配置已备份到 .claude.bw-backup/" -ForegroundColor Green + } + catch { + Write-Host " [ERROR] 克隆失败: $_" -ForegroundColor Red + if (Test-Path $tempClone) { Remove-Item $tempClone -Recurse -Force -ErrorAction SilentlyContinue } + Write-Host " 原始 .claude/ 未被修改" -ForegroundColor Yellow + exit 1 + } + } + } + else { + Write-Host " 首次安装,克隆仓库..." + try { + git clone --depth 1 $GitUrl $ClaudeTarget 2>&1 | ForEach-Object { Write-Host " $_" } + if (-not (Test-Path (Join-Path $ClaudeTarget "CLAUDE.md"))) { + throw "克隆完成但缺少 CLAUDE.md" + } + } + catch { + Write-Host " [ERROR] 克隆失败: $_" -ForegroundColor Red + Write-Host "" + Write-Host " 可能原因:" -ForegroundColor Yellow + Write-Host " - Gitea 服务不可达 (检查 https://code.letcareme.com)" -ForegroundColor Yellow + Write-Host " - 网络连接问题 (检查 DNS 和防火墙)" -ForegroundColor Yellow + Write-Host " - Git 凭证错误 (检查用户名密码)" -ForegroundColor Yellow + Write-Host "" + Write-Host " 离线模式: 如有本地 .claude 备份,运行:" -ForegroundColor Gray + Write-Host " Copy-Item .claude.bw-backup .claude -Recurse" -ForegroundColor Gray + exit 1 + } + } +} +else { + Write-Host "`n[2/6] StartOnly 模式,跳过同步" -ForegroundColor Gray +} + +# 步骤 3: 完整性校验 +$integrityFile = Join-Path $ClaudeTarget "integrity.sha256" +if (Test-Path $integrityFile) { + Write-Host "`n[3/6] 完整性校验..." -ForegroundColor White + $failures = @() + Get-Content $integrityFile | ForEach-Object { + if ($_ -match '^([a-f0-9]{64})\s+(.+)$') { + $expectedHash = $Matches[1] + $filePath = Join-Path $ClaudeTarget $Matches[2] + if (Test-Path $filePath) { + $actualHash = (Get-FileHash $filePath -Algorithm SHA256).Hash.ToLower() + if ($actualHash -ne $expectedHash) { + $failures += $Matches[2] + } + } + } + } + if ($failures.Count -gt 0) { + Write-Host " [WARN] 以下文件哈希不匹配:" -ForegroundColor Yellow + $failures | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow } + Write-Host " 可能原因: 仓库内容被修改或本地有改动" -ForegroundColor Yellow + $continue = Read-Host " 继续? (y/n)" + if ($continue -ne 'y') { exit 1 } + } else { + Write-Host " [OK] 所有文件完整性校验通过" -ForegroundColor Green + } +} else { + Write-Host "`n[3/6] 跳过完整性校验 (无 integrity.sha256)" -ForegroundColor Gray +} + +# 步骤 4: 渲染 settings.json +Write-Host "`n[4/6] 渲染配置模板..." -ForegroundColor White +Render-SettingsTemplate + +# 步骤 5: 确保必要目录存在 +Write-Host "`n[5/6] 初始化本地目录..." -ForegroundColor White +$localDirs = @("debug", "sessions", "cache", "backups", "telemetry", "shell-snapshots", "projects", "memory") +foreach ($d in $localDirs) { + $dirPath = Join-Path $ClaudeTarget $d + if (-not (Test-Path $dirPath)) { + New-Item -ItemType Directory -Path $dirPath -Force | Out-Null + Write-Host " 创建: $d/" -ForegroundColor Gray + } +} + +# 设置环境变量 (进程级) +$env:CLAUDE_HOME = $ClaudeTarget +Write-Host " [OK] CLAUDE_HOME=$ClaudeTarget" -ForegroundColor Green + +# 验证环境变量传递 +$nodeCheck = & node -e "console.log(process.env.CLAUDE_HOME || 'NOT_SET')" 2>$null +if ($nodeCheck -eq $ClaudeTarget) { + Write-Host " [OK] Node.js 环境变量传递验证通过" -ForegroundColor Green +} else { + Write-Host " [WARN] Node.js 环境变量传递异常,hooks 可能无法正确解析路径" -ForegroundColor Yellow +} + +# 步骤 6: 启动 Claude Code +Write-Host "`n[6/6] 启动 Claude Code..." -ForegroundColor White +Write-Host "" +Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Green +Write-Host " ║ Bookworm 就绪! 正在启动 Claude Code... ║" -ForegroundColor Green +Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Green +Write-Host "" + +& claude diff --git a/stop.ps1 b/stop.ps1 new file mode 100644 index 0000000..4985116 --- /dev/null +++ b/stop.ps1 @@ -0,0 +1,145 @@ +<# +.SYNOPSIS + Bookworm Portable - 清理/卸载脚本 +.DESCRIPTION + 清除环境变量, 恢复原始 .claude 目录, 清理痕迹. +.USAGE + .\stop.ps1 # 标准清理 (保留 Bookworm 配置) + .\stop.ps1 -Restore # 完整恢复 (删除 Bookworm, 恢复备份) + .\stop.ps1 -Deep # 深度清理 (含 PS 历史) +#> + +param( + [switch]$Restore, + [switch]$Deep +) + +$ClaudeTarget = Join-Path $env:USERPROFILE ".claude" +$BackupPath = Join-Path $env:USERPROFILE ".claude.bw-backup" + +Write-Host "" +Write-Host " Bookworm Portable - 清理" -ForegroundColor Cyan +Write-Host " ========================" -ForegroundColor Cyan +Write-Host "" + +# 1. 终止 Claude Code 及 Node.js 子进程 +$claudeProcs = Get-Process | Where-Object { $_.ProcessName -in @("claude", "claude-code") } -ErrorAction SilentlyContinue +if ($claudeProcs) { + Write-Host "[1/5] 终止 Claude Code 进程..." -ForegroundColor Yellow + $claudeProcs | Stop-Process -Force + Start-Sleep -Seconds 3 + # 清理残留 node 子进程 (hooks) + Get-Process node -ErrorAction SilentlyContinue | Where-Object { + $_.CommandLine -match '\.claude[\\/]hooks' + } | Stop-Process -Force -ErrorAction SilentlyContinue + Write-Host " [OK] 进程已终止" -ForegroundColor Green +} +else { + Write-Host "[1/5] 无 Claude Code 进程运行" -ForegroundColor Gray +} + +# 2. 清除进程级环境变量 +Write-Host "[2/4] 清除环境变量..." -ForegroundColor White +$envVars = @( + "CLAUDE_HOME", "CLAUDE_ROOT", "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", "SUPABASE_ACCESS_TOKEN", + "GITHUB_PERSONAL_ACCESS_TOKEN", "SLACK_BOT_TOKEN", + "ATLASSIAN_API_TOKEN", "BROWSERBASE_API_KEY", + "FIRECRAWL_API_KEY" +) +foreach ($v in $envVars) { + if ([System.Environment]::GetEnvironmentVariable($v, "Process")) { + [System.Environment]::SetEnvironmentVariable($v, $null, "Process") + Write-Host " 清除: $v" -ForegroundColor Gray + } +} +Write-Host " [OK] 环境变量已清除" -ForegroundColor Green + +# 3. 清除 Git 凭证缓存 +Write-Host "[3/5] 清除 Git 凭证缓存..." -ForegroundColor White +$gitCredTargets = @("git:https://code.letcareme.com", "git:http://8.138.11.105", "git:https://8.138.11.105") +foreach ($target in $gitCredTargets) { + $exists = cmdkey /list 2>$null | Select-String $target + if ($exists) { + cmdkey /delete:$target 2>$null + Write-Host " 清除: $target" -ForegroundColor Gray + } +} +# 通过 git credential reject 清除缓存 +@("code.letcareme.com", "8.138.11.105") | ForEach-Object { + @" +protocol=https +host=$_ +"@ | git credential reject 2>$null +} +Write-Host " [OK] Git 凭证已清除" -ForegroundColor Green + +# 4. 恢复备份 (如果请求) +if ($Restore) { + Write-Host "[4/5] 恢复原始 .claude 目录..." -ForegroundColor White + if (Test-Path $BackupPath) { + if (Test-Path $ClaudeTarget) { + $retries = 3 + while ($retries -gt 0) { + try { + Remove-Item $ClaudeTarget -Recurse -Force -ErrorAction Stop + break + } catch { + $retries-- + if ($retries -gt 0) { + Write-Host " 文件占用,等待重试... ($retries)" -ForegroundColor Yellow + Start-Sleep 3 + } else { + Write-Host " [ERROR] 无法删除 .claude,可能有文件被占用" -ForegroundColor Red + Write-Host " 请关闭所有 Node.js/Claude 进程后重试" -ForegroundColor Yellow + exit 1 + } + } + } + Write-Host " 已删除 Bookworm 配置" -ForegroundColor Gray + } + Rename-Item $BackupPath $ClaudeTarget + Write-Host " [OK] 原始 .claude 已恢复" -ForegroundColor Green + } + else { + Write-Host " [!] 无备份可恢复 (.claude.bw-backup 不存在)" -ForegroundColor Yellow + } +} +else { + Write-Host "[4/5] 保留 Bookworm 配置 (使用 -Restore 可恢复原始)" -ForegroundColor Gray +} + +# 5. 深度清理 (可选) +if ($Deep) { + Write-Host "[5/5] 深度清理..." -ForegroundColor White + + # 清除 PowerShell 历史 + $histFile = Join-Path $env:APPDATA "Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt" + if (Test-Path $histFile) { + # 仅删除含 bookworm/secrets/api_key 的行 + $lines = Get-Content $histFile + $cleaned = $lines | Where-Object { + $_ -notmatch 'secrets\.enc|ANTHROPIC_API_KEY|api[_-]?key|bookworm-portable' + } + Set-Content $histFile -Value $cleaned + Write-Host " [OK] PS 历史已清理敏感条目" -ForegroundColor Green + } + + # 清除 Claude Code 本地缓存 + $cacheDir = Join-Path $env:LOCALAPPDATA "claude-code" + if (Test-Path $cacheDir) { + Write-Host " 发现本地缓存: $cacheDir" -ForegroundColor Yellow + Write-Host " (手动决定是否删除)" -ForegroundColor Yellow + } + + Write-Host " [OK] 深度清理完成" -ForegroundColor Green +} +else { + Write-Host "[5/5] 跳过深度清理 (使用 -Deep 可清理 PS 历史)" -ForegroundColor Gray +} + +Write-Host "" +Write-Host " ╔══════════════════════════════════════════╗" -ForegroundColor Green +Write-Host " ║ Bookworm 已清理完毕 ║" -ForegroundColor Green +Write-Host " ╚══════════════════════════════════════════╝" -ForegroundColor Green +Write-Host ""