Add third-machine-install.ps1 bootstrap

This commit is contained in:
bookworm-admin 2026-04-21 18:08:15 +08:00
parent 1c14c60d3f
commit a365ea9ed1

View File

@ -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 <raw-url> -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='<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 = '<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 }