308 lines
11 KiB
PowerShell
308 lines
11 KiB
PowerShell
|
|
#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 }
|