Add .claude/bootstrap/ (windows-bootstrap.ps1, restore-secrets.ps1, backup-to-bundle.ps1, RESTORE.md) plus machines/guru-5070.md. Idempotent 11-phase rebuild after a clean Windows reset: winget core tools + .NET/WiX, protoc, Poppler, Tailscale; restore SOPS age key/SSH/tool-auth/identity from the E:/F: recovery bundle; clone repos+submodules; set OLLAMA_MODELS/HOST/PROTOC; detect existing D:\OllamaModels; register scheduled tasks. Includes session log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
148 lines
6.6 KiB
PowerShell
148 lines
6.6 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Restore ClaudeTools secrets + machine identity from a recovery bundle
|
|
(produced by the Windows bootstrap backup) back to their real locations.
|
|
|
|
.DESCRIPTION
|
|
Two restore groups:
|
|
[home] -> out-of-repo secrets that live under the user profile
|
|
(SOPS age key, SSH keys, Claude/grok/gemini auth, git config,
|
|
PowerShell profile). These are needed BEFORE cloning repos.
|
|
[repo] -> repo-local, gitignored files that go back into D:\claudetools
|
|
(identity.json, settings.local.json, current-mode, .mcp.json,
|
|
.claude/state, ticktick tokens, dataforth oauth). These require
|
|
the claudetools repo to already be cloned.
|
|
|
|
Idempotent. Only restores files that exist in the bundle. Never overwrites a
|
|
newer file unless -Force is given.
|
|
|
|
.PARAMETER BundlePath
|
|
Path to the recovery bundle root (the folder containing 'secrets' and
|
|
'identity'). Auto-detected from F:\ then E:\ if not supplied.
|
|
|
|
.PARAMETER ClaudeToolsRoot
|
|
Where claudetools is / will be cloned. Default D:\claudetools.
|
|
|
|
.PARAMETER Group
|
|
home | repo | all (default all).
|
|
|
|
.EXAMPLE
|
|
.\restore-secrets.ps1 -Group home # before cloning repos
|
|
.\restore-secrets.ps1 -Group repo # after cloning claudetools
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$BundlePath,
|
|
[string]$ClaudeToolsRoot = 'D:\claudetools',
|
|
[ValidateSet('home','repo','all')][string]$Group = 'all',
|
|
[switch]$Force
|
|
)
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
function Find-Bundle {
|
|
foreach ($d in 'F:','E:','D:') {
|
|
$p = "$d\claudetools-recovery"
|
|
if (Test-Path "$p\secrets") { return $p }
|
|
}
|
|
return $null
|
|
}
|
|
if (-not $BundlePath) { $BundlePath = Find-Bundle }
|
|
if (-not $BundlePath -or -not (Test-Path "$BundlePath\secrets")) {
|
|
throw "Recovery bundle not found. Plug in the drive or pass -BundlePath. Looked for <drive>:\claudetools-recovery\secrets"
|
|
}
|
|
Write-Host "[INFO] Using recovery bundle: $BundlePath" -ForegroundColor Cyan
|
|
|
|
function Restore-One($src, $dst) {
|
|
if (-not (Test-Path -LiteralPath $src)) { Write-Host "[SKIP] not in bundle: $src"; return }
|
|
$parent = Split-Path $dst -Parent
|
|
if ($parent -and -not (Test-Path $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null }
|
|
if ((Test-Path -LiteralPath $dst) -and -not $Force) {
|
|
Write-Host "[KEEP] exists (use -Force to overwrite): $dst" -ForegroundColor Yellow
|
|
return
|
|
}
|
|
Copy-Item -LiteralPath $src -Destination $dst -Force
|
|
Write-Host "[OK] $dst" -ForegroundColor Green
|
|
}
|
|
|
|
# ---------------------------------------------------------------- HOME secrets
|
|
if ($Group -in 'home','all') {
|
|
Write-Host "`n=== Restoring home-profile secrets ===" -ForegroundColor Cyan
|
|
$u = $env:USERPROFILE
|
|
$s = "$BundlePath\secrets"
|
|
|
|
# SOPS age key (CRITICAL - vault is undecryptable without it)
|
|
New-Item -ItemType Directory -Force -Path "$u\.config\sops\age" | Out-Null
|
|
New-Item -ItemType Directory -Force -Path "$env:APPDATA\sops\age" | Out-Null
|
|
Restore-One "$s\sops-age\keys.txt" "$u\.config\sops\age\keys.txt"
|
|
Restore-One "$s\sops-age\keys.txt" "$env:APPDATA\sops\age\keys.txt"
|
|
|
|
# SSH
|
|
New-Item -ItemType Directory -Force -Path "$u\.ssh" | Out-Null
|
|
if (Test-Path "$s\ssh") {
|
|
Get-ChildItem "$s\ssh" -File | ForEach-Object { Restore-One $_.FullName "$u\.ssh\$($_.Name)" }
|
|
# lock down private key perms (remove inheritance, owner-only)
|
|
Get-ChildItem "$u\.ssh" -File | Where-Object { $_.Name -notmatch '\.pub$' -and $_.Name -ne 'known_hosts' -and $_.Name -ne 'config' } | ForEach-Object {
|
|
icacls $_.FullName /inheritance:r /grant:r "$($env:USERNAME):(F)" 2>$null | Out-Null
|
|
}
|
|
}
|
|
|
|
# Claude Code auth/config
|
|
Restore-One "$s\claude\.claude.json" "$u\.claude.json"
|
|
Restore-One "$s\claude\.credentials.json" "$u\.claude\.credentials.json"
|
|
Restore-One "$s\claude\settings.json" "$u\.claude\settings.json"
|
|
Restore-One "$s\claude\keybindings.json" "$u\.claude\keybindings.json"
|
|
Restore-One "$s\claude\statusline-command.sh" "$u\.claude\statusline-command.sh"
|
|
|
|
# grok
|
|
Restore-One "$s\grok\auth.json" "$u\.grok\auth.json"
|
|
Restore-One "$s\grok\config.toml" "$u\.grok\config.toml"
|
|
Restore-One "$s\grok\agent_id" "$u\.grok\agent_id"
|
|
|
|
# gemini
|
|
Restore-One "$s\gemini\oauth_creds.json" "$u\.gemini\oauth_creds.json"
|
|
Restore-One "$s\gemini\google_accounts.json" "$u\.gemini\google_accounts.json"
|
|
Restore-One "$s\gemini\settings.json" "$u\.gemini\settings.json"
|
|
Restore-One "$s\gemini\installation_id" "$u\.gemini\installation_id"
|
|
|
|
# user-global Claude commands + plugins (not in the repo)
|
|
if (Test-Path "$s\claude-global\commands") {
|
|
New-Item -ItemType Directory -Force -Path "$u\.claude\commands" | Out-Null
|
|
Copy-Item "$s\claude-global\commands\*" "$u\.claude\commands\" -Recurse -Force
|
|
Write-Host "[OK] $u\.claude\commands\*" -ForegroundColor Green
|
|
}
|
|
if (Test-Path "$s\claude-global\plugins") {
|
|
New-Item -ItemType Directory -Force -Path "$u\.claude\plugins" | Out-Null
|
|
Copy-Item "$s\claude-global\plugins\*" "$u\.claude\plugins\" -Recurse -Force
|
|
Write-Host "[OK] $u\.claude\plugins\*" -ForegroundColor Green
|
|
}
|
|
|
|
# git global config
|
|
Restore-One "$s\git\.gitconfig" "$u\.gitconfig"
|
|
|
|
# PowerShell profile
|
|
Restore-One "$s\powershell\Microsoft.PowerShell_profile.ps1" $PROFILE
|
|
}
|
|
|
|
# ---------------------------------------------------------------- REPO-local
|
|
if ($Group -in 'repo','all') {
|
|
Write-Host "`n=== Restoring repo-local identity files ===" -ForegroundColor Cyan
|
|
if (-not (Test-Path $ClaudeToolsRoot)) {
|
|
Write-Host "[WARN] $ClaudeToolsRoot does not exist yet. Clone the repo first, then re-run with -Group repo." -ForegroundColor Yellow
|
|
} else {
|
|
$i = "$BundlePath\identity"
|
|
Restore-One "$i\identity.json" "$ClaudeToolsRoot\.claude\identity.json"
|
|
Restore-One "$i\settings.local.json" "$ClaudeToolsRoot\.claude\settings.local.json"
|
|
Restore-One "$i\current-mode" "$ClaudeToolsRoot\.claude\current-mode"
|
|
Restore-One "$i\coord-broadcasts-seen" "$ClaudeToolsRoot\.claude\coord-broadcasts-seen"
|
|
Restore-One "$i\mcp.json" "$ClaudeToolsRoot\.mcp.json"
|
|
Restore-One "$i\ticktick-tokens.json" "$ClaudeToolsRoot\mcp-servers\ticktick\.tokens.json"
|
|
Restore-One "$i\dataforth-oauth.txt" "$ClaudeToolsRoot\clients\dataforth\Oauth.txt"
|
|
if (Test-Path "$i\state") {
|
|
New-Item -ItemType Directory -Force -Path "$ClaudeToolsRoot\.claude\state" | Out-Null
|
|
Copy-Item "$i\state\*" "$ClaudeToolsRoot\.claude\state\" -Recurse -Force
|
|
Write-Host "[OK] $ClaudeToolsRoot\.claude\state\*" -ForegroundColor Green
|
|
}
|
|
}
|
|
}
|
|
Write-Host "`n[DONE] restore-secrets.ps1 ($Group)" -ForegroundColor Cyan
|