bookworm-boot/install.ps1

303 lines
13 KiB
PowerShell
Raw Normal View History

<#
2026-04-01 12:47:44 +08:00
.SYNOPSIS
Bookworm Portable - 安装/启动脚本
.DESCRIPTION
Gitea 私有仓克隆 Bookworm 配置到目标机,
解密凭证, 渲染模板, 启动 Claude Code.
.USAGE
# 首次安装 (从 Gitea 克隆)
.\install.ps1
# 仅启动 (已安装过)
.\install.ps1 -StartOnly
# 指定 Gitea 地址
2026-04-01 14:45:00 +08:00
.\install.ps1 -GitUrl "http://8.138.11.105:3000/bookworm/bookworm-config.git"
2026-04-01 12:47:44 +08:00
#>
param(
2026-04-01 14:45:00 +08:00
[string]$GitUrl = "https://code.letcareme.com/bookworm/bookworm-config.git",
2026-04-01 12:47:44 +08:00
[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) {
$searchPaths = @("C:\Program Files\Git\usr\bin\openssl.exe", "D:\Git\usr\bin\openssl.exe", "D:\Git\mingw64\bin\openssl.exe", "C:\Program Files\Git\mingw64\bin\openssl.exe")
$opensslCmd = $searchPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
2026-04-01 12:47:44 +08:00
}
# ─── 辅助函数 ────────────────────────────────────────
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