diff --git a/tools/third-machine-install.ps1 b/tools/third-machine-install.ps1 new file mode 100644 index 0000000..cae26e5 --- /dev/null +++ b/tools/third-machine-install.ps1 @@ -0,0 +1,307 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Bookworm Smart Assistant 第三台机一键安装+验收脚本 + +.DESCRIPTION + 在干净的 Win10 (Administrator 账户, 或任意用户) 上一步完成: + 1. 预检: Node / Git / 磁盘空间 / 账户路径 + 2. 写入带外公钥到 ~/.bookworm-trust.pem + 3. 设置只读 token 到环境 + 4. 从 Gitea 拉取 tools/bookworm-sync.ps1 (raw) + 5. 运行 sync 安装到 ~/.claude + 6. 验收: 统计 skills/hooks/agents 数量对比目标值 + 7. 打印安装报告 + +.USAGE + # 方法 A: 直接运行 (需先把本脚本存成 install.ps1) + # pwsh -ExecutionPolicy Bypass -File install.ps1 + # 方法 B: 远程下载一键运行 (首次拉脚本走 bootstrap token, 不验签, 合理因为脚本是一次性引导) + # Invoke-WebRequest -Uri -Headers @{Authorization="token $env:BOOKWORM_PULL_TOKEN"} -OutFile install.ps1 + +.NOTES + Version: 1.0.0 (2026-04-21) + 需要 Administrator 或目标账户下运行. + 如需卸载, 删除 ~/.claude 即可 (memory/sessions 也会删, 若需保留请先备份) +#> + +[CmdletBinding()] +param( + [string]$Token = '', + [string]$Ref = 'v6.5.1', + [switch]$SkipInstallVerify +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +function W-Info ($m) { Write-Host "[install] $m" -ForegroundColor Cyan } +function W-Ok ($m) { Write-Host "[install] OK $m" -ForegroundColor Green } +function W-Warn ($m) { Write-Host "[install] !! $m" -ForegroundColor Yellow } +function W-Err ($m) { Write-Host "[install] XX $m" -ForegroundColor Red } + +# ========== 参数 (带外签名公钥明文) ========== +$TRUST_PEM_CONTENT = @' +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAl+maJk051gEFsK8ncj3CaP9ND7r6q5lXCK9eiDUBj1Y= +-----END PUBLIC KEY----- +'@ +$TRUST_FINGERPRINT_EXPECT = '26b83e1b38cdf64a' + +$GITEA_HOST = 'code.letcareme.com' +$REPO = 'bookworm/bookworm-smart-assistant' +$CLAUDE_ROOT = Join-Path $env:USERPROFILE '.claude' +$TRUST_PEM_PATH = Join-Path $env:USERPROFILE '.bookworm-trust.pem' + +# 目标计数 (v6.5.1) +$TARGET_SKILLS = 93 +$TARGET_AGENTS = 18 +$TARGET_HOOKS_MIN = 37 # >=37 即可 (38 全量, 有条件注册容错) + +# ========== 预检 ========== +function Assert-Prereqs { + W-Info "账户: $env:USERNAME | UserProfile: $env:USERPROFILE" + + # Node 18+ + try { + $v = (& node --version) 2>$null + if ($LASTEXITCODE -ne 0) { throw } + W-Info "Node: $v" + $major = [int]($v -replace '^v(\d+).*', '$1') + if ($major -lt 18) { throw "Node 版本过低: $v (需 18+)" } + } catch { + W-Err "Node.js 未装或版本过低. 请先装 https://nodejs.org/" + exit 10 + } + + # Git 2.20+ + try { + $g = (& git --version) 2>$null + if ($LASTEXITCODE -ne 0) { throw } + W-Info "Git: $g" + } catch { + W-Err "Git 未装. 请先装 https://git-scm.com/" + exit 11 + } + + # 磁盘空间 (目标盘 ≥ 500MB) + $drive = (Split-Path -Qualifier $env:USERPROFILE).TrimEnd(':') + $psDrive = Get-PSDrive -Name $drive -ErrorAction SilentlyContinue + if ($psDrive -and $psDrive.Free -lt 500MB) { + W-Warn "目标盘 $drive`: 剩余 $([math]::Round($psDrive.Free/1MB))MB, 建议 >500MB" + } + + # Token + $tok = $Token + if (-not $tok) { $tok = $env:BOOKWORM_PULL_TOKEN } + if (-not $tok) { + W-Err "缺少 Gitea 只读 token. 传 -Token 或设 `$env:BOOKWORM_PULL_TOKEN=''" + exit 13 + } + if ($tok -notmatch '^[a-f0-9]{40}$') { + W-Err "Token 格式不对 (应为 40 位 hex)" + exit 14 + } + W-Ok "预检通过" + return $tok +} + +# ========== 写带外公钥 ========== +function Set-TrustPem { + if (Test-Path $TRUST_PEM_PATH) { + $existing = Get-Content -Raw $TRUST_PEM_PATH + if ($existing.Trim() -eq $TRUST_PEM_CONTENT.Trim()) { + W-Info "公钥已就位 ($TRUST_PEM_PATH)" + return + } + W-Warn "已有不同的 $TRUST_PEM_PATH, 备份 -> .bak" + Copy-Item $TRUST_PEM_PATH "$TRUST_PEM_PATH.bak-$(Get-Date -Format yyyyMMddHHmmss)" -Force + } + # 写 LF 行尾 (Ed25519 验签对行尾敏感, 主机侧 pubkey 是 LF) + $bytes = [System.Text.Encoding]::UTF8.GetBytes(($TRUST_PEM_CONTENT -replace "`r`n", "`n").Trim() + "`n") + [System.IO.File]::WriteAllBytes($TRUST_PEM_PATH, $bytes) + W-Ok "公钥已写入 $TRUST_PEM_PATH" + + # 指纹验证 + $script = @' +const fs = require('fs'); +const crypto = require('crypto'); +const pem = fs.readFileSync(process.argv[2], 'utf8'); +const fp = crypto.createHash('sha256').update(pem).digest('hex').slice(0,16); +process.stdout.write(fp); +'@ + $tmp = New-TemporaryFile + Set-Content $tmp.FullName -Value $script -Encoding UTF8 + $fp = & node $tmp.FullName $TRUST_PEM_PATH + Remove-Item $tmp.FullName -Force + if ($fp -ne $TRUST_FINGERPRINT_EXPECT) { + W-Err "公钥指纹不匹配! 期望=$TRUST_FINGERPRINT_EXPECT 实际=$fp" + W-Err "公钥可能在粘贴中损坏, 拒绝继续" + exit 20 + } + W-Ok "公钥指纹验证: $fp" +} + +# ========== 下载 sync.ps1 ========== +function Get-SyncScript ($tok) { + $url = "https://$GITEA_HOST/$REPO/raw/tag/$Ref/tools/bookworm-sync.ps1" + $dest = Join-Path $env:TEMP "bookworm-sync-bootstrap-$([Guid]::NewGuid().ToString()).ps1" + W-Info "下载 sync.ps1 -> $dest" + try { + Invoke-WebRequest -Uri $url -Headers @{ Authorization = "token $tok" } -OutFile $dest -UseBasicParsing + } catch { + W-Err "下载失败: $_" + exit 25 + } + if (-not (Test-Path $dest) -or (Get-Item $dest).Length -lt 1000) { + W-Err "下载的 sync.ps1 异常 (文件过小或不存在)" + exit 26 + } + W-Ok "sync.ps1 下载完成 ($((Get-Item $dest).Length) 字节)" + return $dest +} + +# ========== 运行 sync ========== +function Invoke-Sync ($syncPath, $tok) { + W-Info "=== 执行 sync.ps1 -Ref $Ref ===" + $env:BOOKWORM_PULL_TOKEN = $tok + & $syncPath -Ref $Ref + if ($LASTEXITCODE -ne 0) { + W-Err "sync 失败 (exit $LASTEXITCODE)" + exit 30 + } + W-Ok "sync 完成" +} + +# ========== 验收 ========== +function Test-Installation { + W-Info "=== 验收 ===" + + # 存在性 + foreach ($d in @('agents', 'hooks', 'skills', 'constitution', 'lib', 'scripts')) { + $p = Join-Path $CLAUDE_ROOT $d + if (-not (Test-Path $p)) { + W-Err "缺失目录: $d" + exit 40 + } + } + W-Ok "6 个关键目录齐全" + + # Skills 数 + $skillsDirs = @(Get-ChildItem -Path (Join-Path $CLAUDE_ROOT 'skills') -Directory -ErrorAction SilentlyContinue | + Where-Object { Test-Path (Join-Path $_.FullName 'SKILL.md') }) + $skillCount = $skillsDirs.Count + if ($skillCount -lt $TARGET_SKILLS) { + W-Warn "Skills 数偏低: $skillCount / $TARGET_SKILLS" + } else { + W-Ok "Skills: $skillCount (目标 $TARGET_SKILLS)" + } + + # Agents 数 + $agentFiles = @(Get-ChildItem -Path (Join-Path $CLAUDE_ROOT 'agents') -Filter '*.md' -File -ErrorAction SilentlyContinue) + $agentCount = $agentFiles.Count + if ($agentCount -lt $TARGET_AGENTS) { + W-Warn "Agents 数偏低: $agentCount / $TARGET_AGENTS" + } else { + W-Ok "Agents: $agentCount (目标 $TARGET_AGENTS)" + } + + # Hooks 数 (从 settings.json 解析 hooks 注册条目) + $settingsPath = Join-Path $CLAUDE_ROOT 'settings.json' + if (Test-Path $settingsPath) { + try { + $settings = Get-Content -Raw $settingsPath | ConvertFrom-Json + $hookCount = 0 + foreach ($hookType in @('UserPromptSubmit', 'SessionStart', 'PreToolUse', 'PostToolUse', 'Stop', 'PreCompact')) { + $grp = $settings.hooks.$hookType + if ($grp) { + foreach ($g in $grp) { + $hookCount += $g.hooks.Count + } + } + } + if ($hookCount -lt $TARGET_HOOKS_MIN) { + W-Warn "Hooks 注册数偏低: $hookCount / $TARGET_HOOKS_MIN+" + } else { + W-Ok "Hooks 注册数: $hookCount (目标 $TARGET_HOOKS_MIN+)" + } + } catch { + W-Err "settings.json 解析失败: $_" + } + } else { + W-Err "settings.json 不存在, 渲染失败?" + exit 41 + } + + # settings.json 无残留占位 + $raw = Get-Content -Raw $settingsPath + if ($raw -match '\{\{[A-Z_]+\}\}') { + W-Err "settings.json 残留未替换占位: $($matches[0])" + exit 42 + } + W-Ok "settings.json 占位全部渲染" + + # 快速功能测试: 运行 integrity-check + $icPath = Join-Path $CLAUDE_ROOT 'hooks/integrity-check.js' + if (Test-Path $icPath) { + & node $icPath 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { + W-Ok "hooks/integrity-check.js 执行 PASS" + } else { + W-Warn "hooks/integrity-check.js 退出码 $LASTEXITCODE (可能首次需 --generate)" + } + } + + # 带外信任文件检查 + if (Test-Path $TRUST_PEM_PATH) { + W-Ok "信任锚点: $TRUST_PEM_PATH" + } + + # MANIFEST + $manifest = $null + if (Test-Path (Join-Path $CLAUDE_ROOT 'MANIFEST.json')) { + $manifest = Get-Content -Raw (Join-Path $CLAUDE_ROOT 'MANIFEST.json') | ConvertFrom-Json + } + + Write-Host "" + Write-Host "=================================================" -ForegroundColor Magenta + Write-Host " Bookworm 第三机安装验收报告" -ForegroundColor Magenta + Write-Host "=================================================" -ForegroundColor Magenta + Write-Host " 主机账户: $env:USERNAME" + Write-Host " 根目录: $CLAUDE_ROOT" + if ($manifest) { + Write-Host " 版本: $($manifest.version)" + Write-Host " 签名指纹: $($manifest.pubKeyFingerprint)" + Write-Host " 导出时间: $($manifest.exportedAt)" + } + Write-Host " Skills: $skillCount (目标 $TARGET_SKILLS)" + Write-Host " Agents: $agentCount (目标 $TARGET_AGENTS)" + Write-Host " Hooks 注册: $hookCount (目标 $TARGET_HOOKS_MIN+)" + Write-Host "=================================================" -ForegroundColor Magenta + Write-Host "" + W-Ok "验收通过 — 第三机已可用" + Write-Host "" + Write-Host "下一步:" -ForegroundColor Yellow + Write-Host " 1. 启动 Claude Code 确认横幅 'Bookworm Smart Assistant v6.5.1' 显示" + Write-Host " 2. 后续更新: `$env:BOOKWORM_PULL_TOKEN = ''; & '$TRUST_PEM_PATH'; .\bookworm-sync.ps1 -Ref v6.5.2" + Write-Host " 3. 如要查看详细日志: -VerboseDiag" + Write-Host "" +} + +# ========== 主 ========== +Write-Host "" +Write-Host "###################################################" -ForegroundColor Magenta +Write-Host "# Bookworm Smart Assistant v6.5.1 Installer #" -ForegroundColor Magenta +Write-Host "# (Third-Machine Bootstrap + Sync + Verify) #" -ForegroundColor Magenta +Write-Host "###################################################" -ForegroundColor Magenta +Write-Host "" + +$tok = Assert-Prereqs +Set-TrustPem +$syncPath = Get-SyncScript $tok +try { + Invoke-Sync $syncPath $tok +} finally { + if (Test-Path $syncPath) { Remove-Item $syncPath -Force -ErrorAction SilentlyContinue } +} +if (-not $SkipInstallVerify) { Test-Installation }