bookworm-smart-assistant/scripts/sync/2-pull-and-rebuild.ps1

216 lines
8.5 KiB
PowerShell
Raw Normal View History

# =============================================================================
# Bookworm .claude 同步 - 阶段 2: 目标机器接收 + 路径改写 + 依赖重建
# 在目标机器 (janson9527us) 上运行
# =============================================================================
[CmdletBinding()]
param(
[string]$SourceIP = '192.168.10.17',
[int]$Port = 8765,
[string]$SourceUser = 'leesu',
[switch]$KeepCredentials, # 保留 .credentials.json (默认会删让其重登)
[switch]$SkipRebuild # 跳过 npm/venv 重建
)
$ErrorActionPreference = 'Stop'
$dst = "$env:USERPROFILE\.claude"
$tmpZip = "$env:TEMP\claude-sync.zip"
$url = "http://${SourceIP}:${Port}/claude-sync.zip"
$targetUser = $env:USERNAME
Write-Host "==================== 阶段 2: 接收 + 重建 ====================" -ForegroundColor Cyan
Write-Host " 源: $url"
Write-Host " 目标: $dst"
Write-Host " 路径改写: $SourceUser$targetUser"
Write-Host ""
# ---------- 0) 前置检查 ----------
Write-Host "[0/6] 前置检查..." -ForegroundColor Yellow
$nodeOk = $null -ne (Get-Command node -ErrorAction SilentlyContinue)
$pythonOk = $null -ne (Get-Command python -ErrorAction SilentlyContinue)
Write-Host " Node.js: $(if ($nodeOk) {'✅ '+(node --version)} else {'❌ 未安装 (hooks 必须有 Node 18+)'})"
Write-Host " Python: $(if ($pythonOk) {'✅ '+(python --version)} else {'⚠️ 未安装 (mcp-servers 重建需要)'})"
if (-not $nodeOk) {
Write-Host "❌ 缺 Node.js, 请先 winget install OpenJS.NodeJS.LTS 后重新打开 PowerShell 再跑" -ForegroundColor Red
exit 1
}
# ---------- 1) 备份并清空目标 ----------
Write-Host "[1/6] 备份并清空目标目录..." -ForegroundColor Yellow
if (Test-Path $dst) {
$bak = "$dst.old.$(Get-Date -Format yyyyMMddHHmmss)"
Move-Item $dst $bak -Force
Write-Host " 旧 .claude 已备份到: $bak"
}
New-Item $dst -ItemType Directory -Force | Out-Null
# ---------- 2) 下载 ----------
Write-Host "[2/6] 下载 zip..." -ForegroundColor Yellow
try {
$progressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $url -OutFile $tmpZip -UseBasicParsing
$progressPreference = 'Continue'
$size = [Math]::Round((Get-Item $tmpZip).Length / 1MB, 1)
Write-Host " ✅ 下载完成: $size MB"
} catch {
Write-Host "❌ 下载失败: $_" -ForegroundColor Red
Write-Host " 检查源机器是否在跑 1-pack-and-serve.ps1 + 防火墙是否放行 $Port" -ForegroundColor Yellow
exit 1
}
# ---------- 3) 解压 ----------
Write-Host "[3/6] 解压..." -ForegroundColor Yellow
Expand-Archive -Path $tmpZip -DestinationPath $dst -Force
Write-Host " ✅ 解压完成"
# ---------- 4) 路径改写 ----------
Write-Host "[4/6] 路径改写 ($SourceUser$targetUser)..." -ForegroundColor Yellow
# 收集要改写的配置文件 (扩展名匹配)
$rewriteTargets = @()
foreach ($pattern in @('*.json','*.md','CLAUDE.md','*.jsonl')) {
$rewriteTargets += Get-ChildItem $dst -Filter $pattern -File -ErrorAction SilentlyContinue
}
# 排除大文件(>5MB 的 jsonl 历史不必改)
$rewriteTargets = $rewriteTargets | Where-Object { $_.Length -lt 5MB }
$rewriteCount = 0
foreach ($f in $rewriteTargets) {
try {
$content = Get-Content $f.FullName -Raw -Encoding UTF8 -ErrorAction Stop
$orig = $content
$content = $content `
-replace [regex]::Escape("C:/Users/$SourceUser/"), "C:/Users/$targetUser/" `
-replace [regex]::Escape("C:\Users\$SourceUser\"), "C:\Users\$targetUser\" `
-replace [regex]::Escape("C:\\Users\\$SourceUser\\"), "C:\\Users\\$targetUser\\" `
-replace [regex]::Escape("/c/Users/$SourceUser/"), "/c/Users/$targetUser/"
if ($content -ne $orig) {
Set-Content $f.FullName $content -Encoding UTF8 -NoNewline
$rewriteCount++
}
} catch {
# 跳过二进制或编码异常的文件
}
}
Write-Host " ✅ 改写 $rewriteCount 个文件"
# 残留检查
$leftovers = Get-ChildItem $dst -Filter "*.json" -File -ErrorAction SilentlyContinue |
Where-Object { $_.Length -lt 5MB } |
Select-String -Pattern "Users[/\\]$SourceUser" -List
if ($leftovers) {
Write-Host " ⚠️ 以下文件仍含 $SourceUser 路径残留:" -ForegroundColor Yellow
$leftovers | ForEach-Object { Write-Host " - $($_.Path)" }
}
# 删除 OAuth 凭证 (机器绑定)
if (-not $KeepCredentials) {
Remove-Item "$dst\.credentials.json" -Force -ErrorAction SilentlyContinue
Write-Host " 🔑 已删 .credentials.json (启动后请 claude login 重登)"
}
# ---------- 5) 重建运行时依赖 ----------
if ($SkipRebuild) {
Write-Host "[5/6] 跳过依赖重建 (-SkipRebuild 已指定)" -ForegroundColor Yellow
} else {
Write-Host "[5/6] 重建运行时依赖..." -ForegroundColor Yellow
# 5.1) 根 node_modules
if (Test-Path "$dst\package.json") {
Write-Host " 📦 根目录 npm install..."
Push-Location $dst
npm install --no-audit --no-fund --silent 2>&1 | Out-Null
Pop-Location
Write-Host " ✅ 完成"
}
# 5.2) 各 skill 的 node_modules
Get-ChildItem "$dst\skills" -Directory -ErrorAction SilentlyContinue | ForEach-Object {
if (Test-Path "$($_.FullName)\package.json") {
Write-Host " 📦 skill: $($_.Name) npm install..."
Push-Location $_.FullName
npm install --no-audit --no-fund --silent 2>&1 | Out-Null
Pop-Location
}
}
# 5.3) 各 mcp-server 的 Python venv
if ($pythonOk) {
Get-ChildItem "$dst\mcp-servers" -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$serverDir = $_.FullName
$serverName = $_.Name
$hasPyProject = (Test-Path "$serverDir\pyproject.toml") -or
(Test-Path "$serverDir\requirements.txt") -or
(Test-Path "$serverDir\setup.py")
if (-not $hasPyProject) { return }
Write-Host " 🐍 mcp-server: $serverName 创建 venv..."
Push-Location $serverDir
try {
python -m venv .venv 2>&1 | Out-Null
$pip = "$serverDir\.venv\Scripts\pip.exe"
& $pip install --upgrade pip --quiet 2>&1 | Out-Null
if (Test-Path "pyproject.toml") {
& $pip install -e . --quiet 2>&1 | Out-Null
} elseif (Test-Path "requirements.txt") {
& $pip install -r requirements.txt --quiet 2>&1 | Out-Null
}
Write-Host "$serverName 完成"
} catch {
Write-Host " ⚠️ $serverName 失败: $_" -ForegroundColor Yellow
} finally {
Pop-Location
}
}
} else {
Write-Host " ⚠️ Python 未安装, 跳过 mcp-servers venv 重建" -ForegroundColor Yellow
}
}
# ---------- 6) 验证 ----------
Write-Host "[6/6] 干跑核心 hooks 验证..." -ForegroundColor Yellow
$coreHooks = @(
'stop-dispatcher.js',
'block-dangerous-commands.js',
'block-sensitive-files.js'
)
$allOk = $true
foreach ($h in $coreHooks) {
$p = "$dst\hooks\$h"
if (-not (Test-Path $p)) {
Write-Host " ❌ 缺失: $h" -ForegroundColor Red
$allOk = $false
continue
}
$output = '{}' | node $p 2>&1
$code = $LASTEXITCODE
# 0 = 通过, 2 = 非阻塞警告 (block-* hooks 常用), 都算 OK
if ($code -in 0,2) {
Write-Host "$h (exit=$code)"
} else {
Write-Host "$h (exit=$code)" -ForegroundColor Red
Write-Host " $output" -ForegroundColor DarkGray
$allOk = $false
}
}
# 清理 zip
Remove-Item $tmpZip -Force -ErrorAction SilentlyContinue
Write-Host ""
if ($allOk) {
Write-Host "============== ✅ 同步完成 ==============" -ForegroundColor Green
Write-Host " 下一步:"
Write-Host " 1. 关闭所有 Claude Code 窗口后重开"
if (-not $KeepCredentials) {
Write-Host " 2. 运行 'claude login' 完成登录"
}
Write-Host " 3. 启动新会话, 应看到 [BOOKWORM_SESSION_START] 横幅"
Write-Host "========================================" -ForegroundColor Green
} else {
Write-Host "============== ⚠️ 同步完成但有问题 ==============" -ForegroundColor Yellow
Write-Host " 有 hook 验证失败, 请检查上面 ❌ 的错误信息"
Write-Host " 备份目录: $env:USERPROFILE\.claude.old.*"
Write-Host "================================================" -ForegroundColor Yellow
}