feat(bootstrap): Windows recovery + reinstall toolkit for GURU-5070

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>
This commit is contained in:
2026-06-06 12:06:09 -07:00
parent 5b9bb949a2
commit 6bb75e9320
6 changed files with 810 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
# ClaudeTools Windows Bootstrap & Recovery Runbook
Rebuild this workstation (GURU-5070, Lenovo Legion Pro 7 16IAX10H) after a clean
Windows reset. Everything here is driven by two scripts in this folder:
- `windows-bootstrap.ps1` — installs tools, restores secrets, clones repos, wires tasks
- `restore-secrets.ps1` — copies secrets/identity from the recovery bundle back into place
The recovery bundle lives on the removable drives:
| Drive | Label | Holds |
|-------|---------|-------|
| **E:** | (FAT32) | `claudetools-recovery\` — secrets + identity + manifests (redundant copy) |
| **F:** | Ventoy | `claudetools-recovery\` — same bundle **plus** `data\` (large client data) |
> F: is also a bootable rescue stick (SystemRescue, Boot Repair) — keep it; it can
> help fix the machine. The bundle lives in `F:\claudetools-recovery\`, Ventoy is untouched.
---
## What's in the bundle (and why it can't just be re-cloned)
`claudetools-recovery\`
- `secrets\`
- `sops-age\keys.txt`**THE most critical file.** The SOPS age private key. Without
it the entire vault (`D:\vault`) is permanently undecryptable. Not stored in any repo.
- `ssh\``id_ed25519` (+pub), `pst-cc-ucg` (+pub), `config`, `known_hosts`
- `claude\``.claude.json`, `.credentials.json` (Claude Code login), settings, keybindings, statusline
- `grok\``auth.json`, `config.toml`, `agent_id`
- `gemini\``oauth_creds.json`, `google_accounts.json`, settings, installation_id
- `git\.gitconfig`, `powershell\Microsoft.PowerShell_profile.ps1`
- `identity\` — repo-local gitignored files: `identity.json`, `settings.local.json`,
`current-mode`, `coord-broadcasts-seen`, `mcp.json`, `.claude/state\`, ticktick tokens, dataforth oauth
- `config\` — Windows Terminal settings, fleet `hosts` file, quote-wizard `.env.production`
- `manifests\``installed-tools.txt`, `ollama-models.txt`, `git-global-config.txt`,
`repos.txt`, `user-environment.reg` / `.txt` (incl. `OLLAMA_MODELS`/`OLLAMA_HOST`/`PROTOC`), `scheduled-tasks\*.xml`
- `data\` (F: only) — large non-Gitea client/project data, repo-relative paths
Everything else (all tracked code, skills, commands, docs, session logs, wiki) comes
back from Gitea on clone — no need to back it up.
---
## Fast path (one shot)
From an **elevated PowerShell**, with E: or F: plugged in:
```powershell
# copy the script off the drive first (so it survives a re-clone)
Copy-Item F:\claudetools-recovery\bootstrap\windows-bootstrap.ps1 $env:TEMP\boot.ps1
& $env:TEMP\.. # or just run directly:
F:\claudetools-recovery\bootstrap\windows-bootstrap.ps1 -SkipModels
```
`-SkipModels` defers the ~50 GB Ollama downloads. Drop it (or run Phase 8 later) when
you want them. Add `-RestoreData` to also pull back the large client data from `F:\...\data`.
The script is **idempotent** — safe to re-run; it skips anything already done. To run
just part of it: `-OnlyPhases "1,2,3"`.
---
## Manual path (if you'd rather do it by hand)
1. **Install App Installer** (winget) from the Microsoft Store if missing.
2. **Core tools** (winget ids):
`Git.Git`, `OpenJS.NodeJS.LTS`, `Python.Python.3.14`, `Rustlang.Rustup`,
`Microsoft.VisualStudioCode`, `Ollama.Ollama`, `jqlang.jq`,
`SecretsOPerationS.SOPS`, `FiloSottile.age`, `GitHub.cli`, `AgileBits.1Password.CLI`,
`Microsoft.DotNet.SDK.8`, `Google.Protobuf`, `oschwartz10612.Poppler`, `Tailscale.Tailscale`
Then `dotnet tool install --global wix` (MSI builds).
Set env: `OLLAMA_MODELS=D:\OllamaModels`, `OLLAMA_HOST=0.0.0.0:11434`, `PROTOC=<protoc.exe>`.
3. **AI CLIs:**
- Claude: `irm https://claude.ai/install.ps1 | iex``~/.local/bin/claude.exe`
- Gemini: `npm install -g @google/gemini-cli`
- Grok: `bash -c "curl -fsSL https://x.ai/cli/install.sh | bash"` (Git Bash)
4. **Restore home secrets:** `F:\claudetools-recovery\bootstrap\restore-secrets.ps1 -Group home`
5. **Clone repos:**
```
git clone https://git.azcomputerguru.com/azcomputerguru/claudetools.git D:\claudetools
cd D:\claudetools; git submodule update --init --recursive
git clone https://git.azcomputerguru.com/azcomputerguru/vault.git D:\vault
```
(On-network you can use `http://172.16.3.20:3000/...` to bypass the SSL-renewal blips.)
6. **Restore identity:** `restore-secrets.ps1 -Group repo`
7. **Ollama models (proper set for this 12 GB-VRAM laptop):**
`ollama pull nomic-embed-text:latest` (GrepAI embeddings) and `ollama pull qwen3:8b` (prose_model).
Models live on `D:\OllamaModels` (47.8 GB) — **if D: survived the reset they're already there, skip this.**
Heavy extras (`qwen3:14b`, `codestral:22b`, `qwen3.6:latest`) are opt-in only; they over-saturate 12 GB VRAM.
8. **Scheduled tasks:** import each XML in `manifests\scheduled-tasks\` via
`Register-ScheduledTask -Xml (Get-Content x.xml -Raw) -TaskName "..."`.
9. **Verify:** `D:\claudetools\.claude\scripts\onboarding-diagnostic.ps1`, then `/self-check` in Claude Code.
---
## Post-install: things that need an interactive login
Auth tokens are backed up, but some expire. If a tool says it's unauthenticated:
- **Claude Code:** run `claude`, then `/login` (browser).
- **GitHub CLI:** `gh auth login`
- **1Password:** `op signin`
- **Gemini:** launch `gemini`, complete the Google OAuth browser flow.
- **Grok:** `grok login` (tokens expire after 7 days).
- **Gitea git push:** uses the Windows Credential Manager (`credential.helper=manager`).
First push prompts for the shared `azcomputerguru` account. **Do NOT** bake the password
into the remote URL (the old `D:\work\gururmm` clone did — reset it to a clean URL).
## Verify the vault decrypts (proves the age key restored correctly)
```
bash D:/claudetools/.claude/scripts/vault.sh list
bash D:/claudetools/.claude/scripts/vault.sh get-field projects/claudetools/database.sops.yaml credentials.password
```
If that returns the password, recovery succeeded. If it errors about decryption, the
age key at `%APPDATA%\sops\age\keys.txt` and `~/.config/sops/age/keys.txt` is missing/wrong.
---
## Refreshing this bundle later
Re-run the backup any time (it's just file copies):
`D:\claudetools\.claude\bootstrap\backup-to-bundle.ps1` (writes to E: and F:).

View File

@@ -0,0 +1,133 @@
<#
.SYNOPSIS
Back up ClaudeTools secrets + identity (and optionally large client data) to a
recovery bundle on a removable drive. The inverse of restore-secrets.ps1.
.DESCRIPTION
Captures everything that will NOT come back from a `git clone`:
- out-of-repo secrets under the user profile (age key, ssh, tool auth, git, PS profile)
- repo-local gitignored identity files
- environment manifests (installed tools, ollama models, scheduled-task XML, vscode ext)
- (optional) large gitignored client/project data clusters
Safe to re-run; it refreshes the bundle in place.
.PARAMETER Drives Target drive roots. Default 'E:','F:' (writes the small bundle to both).
.PARAMETER IncludeData Also copy the large client-data clusters (only to the FIRST drive with room; exFAT recommended).
.PARAMETER ClaudeToolsRoot Default D:\claudetools.
.EXAMPLE
.\backup-to-bundle.ps1 # secrets+identity+manifests to E: and F:
.\backup-to-bundle.ps1 -IncludeData # also large data (to F:)
#>
[CmdletBinding()]
param(
[string[]]$Drives = @('E:','F:'),
[switch]$IncludeData,
[string]$ClaudeToolsRoot = 'D:\claudetools',
[string]$DataDrive = 'F:'
)
$ErrorActionPreference = 'Stop'
$u = $env:USERPROFILE
function Save($src,$dst){
if (Test-Path -LiteralPath $src) {
$p = Split-Path $dst -Parent; if (-not (Test-Path $p)) { New-Item -ItemType Directory -Force -Path $p | Out-Null }
Copy-Item -LiteralPath $src -Destination $dst -Force; Write-Host "[OK] $src"
} else { Write-Host "[MISS] $src" }
}
# Build the bundle once under the first available target, then mirror to the rest.
$primary = $Drives | Where-Object { Test-Path "$_\" } | Select-Object -First 1
if (-not $primary) { throw "None of the target drives are accessible: $($Drives -join ', ')" }
$root = "$primary\claudetools-recovery"
Write-Host "=== building bundle at $root ===" -ForegroundColor Cyan
foreach ($d in 'secrets\sops-age','secrets\ssh','secrets\claude','secrets\grok','secrets\gemini','secrets\git','secrets\powershell','identity\state','manifests\scheduled-tasks','bootstrap') {
New-Item -ItemType Directory -Force -Path "$root\$d" | Out-Null
}
# --- secrets ---
Save "$u\.config\sops\age\keys.txt" "$root\secrets\sops-age\keys.txt"
if (Test-Path "$u\.ssh") { Copy-Item "$u\.ssh\*" "$root\secrets\ssh\" -Force; Write-Host "[OK] ~/.ssh/*" }
Save "$u\.claude.json" "$root\secrets\claude\.claude.json"
Save "$u\.claude\.credentials.json" "$root\secrets\claude\.credentials.json"
Save "$u\.claude\settings.json" "$root\secrets\claude\settings.json"
Save "$u\.claude\keybindings.json" "$root\secrets\claude\keybindings.json"
Save "$u\.claude\statusline-command.sh" "$root\secrets\claude\statusline-command.sh"
Save "$u\.grok\auth.json" "$root\secrets\grok\auth.json"
Save "$u\.grok\config.toml" "$root\secrets\grok\config.toml"
Save "$u\.grok\agent_id" "$root\secrets\grok\agent_id"
Save "$u\.gemini\oauth_creds.json" "$root\secrets\gemini\oauth_creds.json"
Save "$u\.gemini\google_accounts.json" "$root\secrets\gemini\google_accounts.json"
Save "$u\.gemini\settings.json" "$root\secrets\gemini\settings.json"
Save "$u\.gemini\installation_id" "$root\secrets\gemini\installation_id"
Save "$u\.gitconfig" "$root\secrets\git\.gitconfig"
# user-global Claude commands + plugins (not in repo)
if (Test-Path "$u\.claude\commands") { New-Item -ItemType Directory -Force -Path "$root\secrets\claude-global\commands" | Out-Null; robocopy "$u\.claude\commands" "$root\secrets\claude-global\commands" /E /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null; Write-Host "[OK] ~/.claude/commands" }
if (Test-Path "$u\.claude\plugins") { New-Item -ItemType Directory -Force -Path "$root\secrets\claude-global\plugins" | Out-Null; robocopy "$u\.claude\plugins" "$root\secrets\claude-global\plugins" /E /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null; Write-Host "[OK] ~/.claude/plugins" }
Save $PROFILE "$root\secrets\powershell\Microsoft.PowerShell_profile.ps1"
# --- repo-local identity ---
Save "$ClaudeToolsRoot\.claude\identity.json" "$root\identity\identity.json"
Save "$ClaudeToolsRoot\.claude\settings.local.json" "$root\identity\settings.local.json"
Save "$ClaudeToolsRoot\.claude\current-mode" "$root\identity\current-mode"
Save "$ClaudeToolsRoot\.claude\coord-broadcasts-seen" "$root\identity\coord-broadcasts-seen"
Save "$ClaudeToolsRoot\.mcp.json" "$root\identity\mcp.json"
Save "$ClaudeToolsRoot\mcp-servers\ticktick\.tokens.json" "$root\identity\ticktick-tokens.json"
Save "$ClaudeToolsRoot\clients\dataforth\Oauth.txt" "$root\identity\dataforth-oauth.txt"
if (Test-Path "$ClaudeToolsRoot\.claude\state") { Copy-Item "$ClaudeToolsRoot\.claude\state\*" "$root\identity\state\" -Recurse -Force -ErrorAction SilentlyContinue }
# --- bootstrap scripts (so the drive is self-contained) ---
Copy-Item "$ClaudeToolsRoot\.claude\bootstrap\*.ps1" "$root\bootstrap\" -Force -ErrorAction SilentlyContinue
Copy-Item "$ClaudeToolsRoot\.claude\bootstrap\RESTORE.md" "$root\bootstrap\" -Force -ErrorAction SilentlyContinue
# --- manifests ---
$m = "$root\manifests"
$tools = 'node','npm','claude','gemini','grok','ollama','py','git','gh','jq','sops','age','cargo','rustc','code','op'
($tools | ForEach-Object { $c = Get-Command $_ -ErrorAction SilentlyContinue; if ($c) { $v = try { (& $_ --version 2>$null | Select-Object -First 1) } catch {''}; "{0,-10} {1,-55} {2}" -f $_,$c.Source,$v } else { "{0,-10} NOT INSTALLED" -f $_ } }) | Out-File "$m\installed-tools.txt" -Encoding utf8
ollama list 2>$null | Out-File "$m\ollama-models.txt" -Encoding utf8
git config --global --list | Out-File "$m\git-global-config.txt" -Encoding utf8
$ext = & code --list-extensions 2>$null; if ($ext) { $ext | Out-File "$m\vscode-extensions.txt" -Encoding utf8 }
foreach ($tn in "GrepAI Watcher - claudetools","ClaudeTools - Orphaned Session Detector","ClaudeTools - KSTEEN SmartBadge Daily") {
$safe = ($tn -replace '[^\w\-]','_')
try { Export-ScheduledTask -TaskName $tn 2>$null | Out-File "$m\scheduled-tasks\$safe.xml" -Encoding utf8 } catch {}
}
# user environment vars (.reg restorable + readable)
reg export "HKCU\Environment" "$m\user-environment.reg" /y 2>$null | Out-Null
(Get-Item 'HKCU:\Environment' | Select-Object -ExpandProperty Property | ForEach-Object { "{0}={1}" -f $_, (Get-ItemProperty 'HKCU:\Environment' -Name $_).$_ }) | Out-File "$m\user-environment.txt" -Encoding utf8
# --- machine config (Windows Terminal, hosts, repo-local real .env files) ---
New-Item -ItemType Directory -Force -Path "$root\config" | Out-Null
$wt = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json"
if (Test-Path $wt) { Save $wt "$root\config\windows-terminal-settings.json" }
Save "$env:WINDIR\System32\drivers\etc\hosts" "$root\config\hosts"
Save "$ClaudeToolsRoot\projects\msp-tools\quote-wizard\frontend\.env.production" "$root\config\quote-wizard.frontend.env.production"
# --- large data (optional) ---
if ($IncludeData) {
$base = "$DataDrive\claudetools-recovery\data"
$xd = @('node_modules','.venv','venv','__pycache__','target','.grepai','.pytest_cache','dist','build')
$xf = @('Thumbs.db','desktop.ini','*.pyc','*.mp3') # radio-show MP3s live on IX Web Hosting - not backed up here
$clusters = @(
'clients\valleywide\app-modernization\source-analysis',
'clients\grabb-durando\ai-demand-review',
'projects\dataforth-dos\datasheet-pipeline',
'projects\dataforth-dos\dfwds-research',
'projects\radio-show\audio-processor'
)
Write-Host "=== copying large data to $base ===" -ForegroundColor Cyan
foreach ($c in $clusters) {
if (Test-Path "$ClaudeToolsRoot\$c") { robocopy "$ClaudeToolsRoot\$c" "$base\$c" /E /R:1 /W:1 /XD $xd /XF $xf /NFL /NDL /NP | Out-Null; Write-Host "[OK] $c" }
}
}
# --- mirror small bundle to the other drives ---
foreach ($d in $Drives) {
if ($d -eq $primary) { continue }
if (Test-Path "$d\") {
Write-Host "=== mirroring bundle -> $d\claudetools-recovery ===" -ForegroundColor Cyan
robocopy $root "$d\claudetools-recovery" /E /R:1 /W:1 /XD data /NFL /NDL /NP | Out-Null
Write-Host "[OK] mirrored to $d"
}
}
Write-Host "`n[DONE] backup-to-bundle.ps1" -ForegroundColor Green

View File

@@ -0,0 +1,147 @@
<#
.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

View File

@@ -0,0 +1,268 @@
<#
.SYNOPSIS
ClaudeTools Windows bootstrap - rebuild a workstation after a clean OS reset.
.DESCRIPTION
Installs every tool ClaudeTools needs, restores secrets + identity from the
recovery bundle, clones the repos, wires up scheduled tasks, and verifies.
Designed to be run top-to-bottom on a fresh Windows 11 install. Idempotent:
re-running skips anything already present.
ORDER OF OPERATIONS (each phase depends on the previous):
0. Preflight - winget, execution policy, UTF-8
1. Core tooling - git, node, python, rust, vscode, ollama, jq, sops, age, gh, op
2. PATH refresh - make freshly-installed tools callable this session
3. AI CLIs - claude (native), gemini (npm), grok (git-bash installer)
4. Restore secrets - age key, ssh, tool auth, git config, PS profile [home group]
5. Clone repos - claudetools + vault + submodules
6. Restore identity - identity.json, settings.local, .mcp.json, state [repo group]
7. Python deps - pip installs for MCP servers / scripts
8. Ollama models - pull qwen/codestral/nomic (optional, large)
9. Scheduled tasks - GrepAI watcher, orphan detector, smartbadge
10. Large data - restore client data from bundle (optional)
11. Verify - onboarding diagnostic
.PARAMETER BundlePath
Recovery bundle root (folder containing 'secrets'/'identity'). Auto-detect F:\ then E:\.
.PARAMETER SkipModels Skip the multi-GB ollama model pulls.
.PARAMETER RestoreData Also restore the large client data from <bundle>\data.
.PARAMETER GiteaHost Gitea base URL. Default git.azcomputerguru.com (use 172.16.3.20:3000 on-network).
.PARAMETER OnlyPhases Comma list of phase numbers to run (e.g. "1,2,3"). Default: all.
.EXAMPLE
# full rebuild, skip giant model downloads for now
.\windows-bootstrap.ps1 -SkipModels
.NOTES
Run from an elevated PowerShell for cleanest winget machine-scope installs,
though most packages also install at user scope without admin.
#>
[CmdletBinding()]
param(
[string]$BundlePath,
[switch]$SkipModels,
[switch]$RestoreData,
[string]$GiteaHost = 'https://git.azcomputerguru.com',
[string]$ClaudeToolsRoot = 'D:\claudetools',
[string]$VaultRoot = 'D:\vault',
[string]$OnlyPhases
)
$ErrorActionPreference = 'Stop'
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
function Phase($n,$title){ if ($OnlyPhases -and ($OnlyPhases -split ',').Trim() -notcontains "$n") { return $false }; Write-Host "`n========== PHASE $n : $title ==========" -ForegroundColor Cyan; return $true }
function Info($m){ Write-Host "[INFO] $m" }
function Ok($m){ Write-Host "[OK] $m" -ForegroundColor Green }
function Warn($m){ Write-Host "[WARN] $m" -ForegroundColor Yellow }
function Have($cmd){ [bool](Get-Command $cmd -ErrorAction SilentlyContinue) }
function Refresh-Path { $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User') }
function Find-Bundle {
if ($BundlePath -and (Test-Path "$BundlePath\secrets")) { return $BundlePath }
foreach ($d in 'F:','E:','D:') { if (Test-Path "$d\claudetools-recovery\secrets") { return "$d\claudetools-recovery" } }
return $null
}
# ============================================================ PHASE 0
if (Phase 0 'Preflight') {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force } catch {}
if (-not (Have winget)) { throw "winget not found. Install 'App Installer' from the Microsoft Store, then re-run." }
Ok "winget present: $((Get-Command winget).Source)"
$script:Bundle = Find-Bundle
if ($script:Bundle) { Ok "recovery bundle: $script:Bundle" } else { Warn "no recovery bundle found - secret/identity restore phases will be skipped" }
}
# ============================================================ PHASE 1
if (Phase 1 'Core tooling (winget)') {
$pkgs = @(
@{id='Git.Git'; cmd='git'},
@{id='OpenJS.NodeJS.LTS'; cmd='node'},
@{id='Python.Python.3.14'; cmd='py'},
@{id='Rustlang.Rustup'; cmd='cargo'},
@{id='Microsoft.VisualStudioCode'; cmd='code'},
@{id='Ollama.Ollama'; cmd='ollama'},
@{id='jqlang.jq'; cmd='jq'},
@{id='SecretsOPerationS.SOPS'; cmd='sops'},
@{id='FiloSottile.age'; cmd='age'},
@{id='GitHub.cli'; cmd='gh'},
@{id='AgileBits.1Password.CLI'; cmd='op'},
@{id='Microsoft.DotNet.SDK.8'; cmd='dotnet'}, # MSI builds / wix
@{id='Google.Protobuf'; cmd='protoc'}, # gururmm prost builds (PROTOC env)
@{id='oschwartz10612.Poppler'; cmd='pdftoppm'}, # dataforth datasheet PDF pipeline
@{id='Tailscale.Tailscale'; cmd='tailscale'} # fleet connectivity (100.x mesh)
)
foreach ($p in $pkgs) {
if (Have $p.cmd) { Ok "$($p.cmd) already installed"; continue }
Info "installing $($p.id) ..."
winget install --id $p.id --exact --silent --accept-package-agreements --accept-source-agreements --disable-interactivity
if ($LASTEXITCODE -ne 0) { Warn "winget returned $LASTEXITCODE for $($p.id) (may already be installed or need elevation)" }
}
Refresh-Path
}
# ============================================================ PHASE 2
if (Phase 2 'PATH refresh') {
Refresh-Path
foreach ($c in 'git','node','npm','py','cargo','jq','sops','age','gh','op','ollama','code','dotnet','protoc','tailscale') {
if (Have $c) { Ok "$c -> $((Get-Command $c).Source)" } else { Warn "$c still not on PATH (open a new shell after install)" }
}
# PROTOC env var for Rust prost builds (path is version-specific, so resolve it live)
$protoc = (Get-Command protoc -ErrorAction SilentlyContinue).Source
if ($protoc) { [Environment]::SetEnvironmentVariable('PROTOC',$protoc,'User'); $env:PROTOC=$protoc; Ok "PROTOC=$protoc" }
}
# ============================================================ PHASE 3
if (Phase 3 'AI CLIs') {
# Claude Code - official native installer -> %USERPROFILE%\.local\bin\claude.exe
if (Have claude) { Ok "claude already installed" } else {
Info "installing Claude Code (native installer)"
try { irm https://claude.ai/install.ps1 | iex } catch { Warn "claude install failed: $_ (manual: irm https://claude.ai/install.ps1 | iex)" }
}
# Gemini CLI - npm global
if (Have gemini) { Ok "gemini already installed" } else {
Info "installing @google/gemini-cli"
npm install -g @google/gemini-cli
}
# Grok CLI - xAI installer (bash; needs Git Bash from Phase 1)
if (Have grok) { Ok "grok already installed" } else {
$bash = 'C:\Program Files\Git\bin\bash.exe'
if (Test-Path $bash) { Info "installing grok via $bash"; & $bash -lc "curl -fsSL https://x.ai/cli/install.sh | bash" }
else { Warn "Git Bash not found; install Git first, then: bash -c 'curl -fsSL https://x.ai/cli/install.sh | bash'" }
}
Refresh-Path
$env:Path += ";$env:USERPROFILE\.local\bin;$env:USERPROFILE\.grok\bin;$env:APPDATA\npm"
}
# ============================================================ PHASE 4
if (Phase 4 'Restore home secrets + machine config') {
if ($script:Bundle) {
& "$here\restore-secrets.ps1" -BundlePath $script:Bundle -Group home
# Stable machine env vars (NOT a blanket reg import - the saved PATH has stale
# version-pinned winget paths. user-environment.reg is kept as reference only.)
[Environment]::SetEnvironmentVariable('OLLAMA_MODELS','D:\OllamaModels','User'); $env:OLLAMA_MODELS='D:\OllamaModels'
[Environment]::SetEnvironmentVariable('OLLAMA_HOST','0.0.0.0:11434','User'); $env:OLLAMA_HOST='0.0.0.0:11434'
Ok "set OLLAMA_MODELS=D:\OllamaModels, OLLAMA_HOST=0.0.0.0:11434"
# Windows Terminal settings
$wtDst = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json"
if (Test-Path "$script:Bundle\config\windows-terminal-settings.json") {
$p = Split-Path $wtDst -Parent
if (Test-Path $p) { Copy-Item "$script:Bundle\config\windows-terminal-settings.json" $wtDst -Force; Ok "Windows Terminal settings restored" }
else { Warn "Windows Terminal not installed yet - restore its settings.json later from config\" }
}
# hosts file (fleet Tailscale MagicDNS entries) - needs admin; merge note only
if (Test-Path "$script:Bundle\config\hosts") {
Warn "fleet hosts entries are in config\hosts - merge into $env:WINDIR\System32\drivers\etc\hosts as admin if Tailscale MagicDNS isn't resolving"
}
}
else { Warn "no bundle - skipping. Restore the SOPS age key + SSH keys manually or the vault will not decrypt." }
}
# ============================================================ PHASE 5
if (Phase 5 'Clone repos') {
if (-not (Test-Path "$ClaudeToolsRoot\.git")) {
Info "cloning claudetools -> $ClaudeToolsRoot"
git clone "$GiteaHost/azcomputerguru/claudetools.git" $ClaudeToolsRoot
Push-Location $ClaudeToolsRoot
Info "initializing submodules (gururmm / guruconnect)"
git submodule update --init --recursive
Pop-Location
} else { Ok "claudetools repo already present" }
if (-not (Test-Path "$VaultRoot\.git")) {
Info "cloning vault -> $VaultRoot"
git clone "$GiteaHost/azcomputerguru/vault.git" $VaultRoot
} else { Ok "vault repo already present" }
# safe.directory entries (mirror the prior machine)
foreach ($d in $ClaudeToolsRoot,$VaultRoot,"$ClaudeToolsRoot/projects/msp-tools/guru-rmm") {
git config --global --add safe.directory ($d -replace '\\','/') 2>$null
}
}
# ============================================================ PHASE 6
if (Phase 6 'Restore repo-local identity') {
if ($script:Bundle) { & "$here\restore-secrets.ps1" -BundlePath $script:Bundle -Group repo -ClaudeToolsRoot $ClaudeToolsRoot }
else { Warn "no bundle - you must hand-create .claude/identity.json (see CLAUDE.md multi-user section)" }
}
# ============================================================ PHASE 7
if (Phase 7 'Python deps + .NET tools') {
# WiX toolset (MSI builds, e.g. gururmm agent) - dotnet global tool
if (Have dotnet) {
if (dotnet tool list --global 2>$null | Select-String '\bwix\b') { Ok "wix tool already installed" }
else { Info "installing wix dotnet tool"; dotnet tool install --global wix 2>$null }
}
if (Have py) {
py -m pip install --upgrade pip 2>$null
$reqs = Get-ChildItem $ClaudeToolsRoot -Recurse -Filter 'requirements*.txt' -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch '\\(node_modules|\.venv|venv|target)\\' }
foreach ($r in $reqs) { Info "pip install -r $($r.FullName)"; py -m pip install -r $r.FullName 2>$null }
# baseline libs used by helper scripts / MCP
py -m pip install requests paramiko mcp 2>$null
Ok "python deps installed (best-effort)"
} else { Warn "py launcher missing - skip" }
}
# ============================================================ PHASE 8
if (Phase 8 'Ollama models') {
# Proper model set for THIS machine (Legion Pro 7, RTX 5070 Ti = 12 GB VRAM):
# nomic-embed-text - REQUIRED for GrepAI semantic search (embeddings)
# qwen3:8b - configured prose_model (identity.json)
# The heavier models (qwen3:14b 9GB, codestral:22b 12GB, qwen3.6:latest 23GB)
# over-saturate 12 GB VRAM on a laptop. Pull them by hand only if needed:
# ollama pull qwen3:14b # codestral:22b # qwen3.6:latest
$models = @('nomic-embed-text:latest','qwen3:8b')
if ($SkipModels) { Warn "-SkipModels set, skipping model pulls" }
elseif (Have ollama) {
# Models live on D:\OllamaModels. If D: survived the reset they are already here.
if (-not $env:OLLAMA_MODELS) { [Environment]::SetEnvironmentVariable('OLLAMA_MODELS','D:\OllamaModels','User'); $env:OLLAMA_MODELS='D:\OllamaModels' }
$have = (ollama list 2>$null | Out-String)
foreach ($m in $models) {
$short = $m -replace ':latest$',''
if ($have -match [regex]::Escape($short)) { Ok "$m already present on D:\OllamaModels (no download)" }
else { Info "ollama pull $m"; ollama pull $m }
}
} else { Warn "ollama missing - skip" }
}
# ============================================================ PHASE 9
if (Phase 9 'Scheduled tasks') {
$tdir = "$script:Bundle\manifests\scheduled-tasks"
if ($script:Bundle -and (Test-Path $tdir)) {
Get-ChildItem $tdir -Filter *.xml | ForEach-Object {
$name = ($_.BaseName -replace '_',' ')
try {
$xml = Get-Content $_.FullName -Raw
Register-ScheduledTask -TaskName $name -Xml $xml -Force -ErrorAction Stop | Out-Null
Ok "registered task: $name"
} catch { Warn "task '$name' import failed: $($_.Exception.Message) (paths/user may differ - re-create manually)" }
}
} else { Warn "no exported tasks in bundle - skip (see manifests\scheduled-tasks)" }
}
# ============================================================ PHASE 10
if (Phase 10 'Large client data (optional)') {
if ($RestoreData -and $script:Bundle -and (Test-Path "$script:Bundle\data")) {
Info "restoring large data $script:Bundle\data -> $ClaudeToolsRoot"
robocopy "$script:Bundle\data" $ClaudeToolsRoot /E /R:1 /W:1 /NFL /NDL /NP | Out-Null
Ok "large data restored"
} else { Warn "skipped (pass -RestoreData to restore client data clusters)" }
}
# ============================================================ PHASE 11
if (Phase 11 'Verify') {
$diag = "$ClaudeToolsRoot\.claude\scripts\onboarding-diagnostic.ps1"
if (Test-Path $diag) { Info "running onboarding diagnostic"; & $diag }
else { Warn "diagnostic not found - run '/self-check' inside Claude Code to verify wiring" }
Write-Host "`n[NEXT] Interactive logins that may need a refresh (tokens expire):" -ForegroundColor Cyan
Write-Host " claude (if .credentials.json expired: run 'claude' and /login)"
Write-Host " gh auth login op signin gemini (browser) grok login"
Write-Host " Verify vault: bash $ClaudeToolsRoot/.claude/scripts/vault.sh list"
}
Write-Host "`n[DONE] windows-bootstrap.ps1 complete." -ForegroundColor Green

View File

@@ -0,0 +1,67 @@
# Machine: GURU-5070 (Windows)
**Hostname:** GURU-5070
**User:** Mike Swanson (mike) — admin
**Platform:** Windows 11 Pro 10.0.26200
**Last Updated:** 2026-06-06
> Same physical hardware as `acg-guru-5070.md` (Lenovo Legion Pro 7 16IAX10H) —
> that profile documents the prior CachyOS Linux install. This box now runs Windows.
---
## Hardware
| Spec | Value |
|------|-------|
| Model | Lenovo Legion Pro 7 16IAX10H (DMI 83F5) |
| CPU | Intel Core Ultra 9 275HX (24 cores) |
| Memory | 32 GB DDR5 |
| GPU | NVIDIA GeForce RTX 5070 Ti Laptop (12 GB) |
| Disks | C: 952 GB NVMe (OS), D: 953 GB NVMe (dev — `D:\claudetools`, `D:\vault`, `D:\work`) |
## Paths
| What | Where |
|------|-------|
| ClaudeTools | `D:\claudetools` |
| Vault | `D:\vault` |
| Other repos | `D:\work\gururmm` |
| SOPS age key | `%APPDATA%\sops\age\keys.txt` and `~\.config\sops\age\keys.txt` |
| Claude CLI | `~\.local\bin\claude.exe` (native installer) |
| Grok CLI | `~\.grok\bin\grok.exe` |
| Gemini CLI | npm global (`@google/gemini-cli`) |
## Toolchain (as of 2026-06-06)
node 24.x · npm 11.x · py/Python 3.14 · git 2.53 · cargo/rustc 1.96 ·
ollama 0.30.6 · jq 1.8 · sops 3.7→3.12 · age 1.3 · op 2.33 · VS Code 1.113 ·
claude 2.1.x · gemini 0.45 · grok 0.2.x. **gh was missing** — bootstrap installs it.
Ollama models: `nomic-embed-text`, `qwen3:8b`, `qwen3:14b`, `codestral:22b`, `qwen3.6:latest`.
## Scheduled tasks (ClaudeTools)
- `GrepAI Watcher - claudetools``D:\claudetools\grepai.exe watch --background` (logon)
- `ClaudeTools - Orphaned Session Detector``py detect_orphaned_sessions.py` (logon + daily)
- `ClaudeTools - KSTEEN SmartBadge Daily` → git-bash `check-ksteen-smartbadge.sh` (daily)
## Capabilities
- [x] Git / Gitea, SSH to infra
- [x] GrepAI watcher
- [x] Ollama local AI (RTX 5070 Ti — light/inference OK)
- [x] MCP: ticktick, grepai
- [x] claude / gemini / grok CLIs (fleet host for all three)
## Recovery
Full rebuild after a reset: `.claude\bootstrap\RESTORE.md`.
Recovery bundle on **E:** and **F:** (`\claudetools-recovery\`). Refresh it with
`.claude\bootstrap\backup-to-bundle.ps1`.
## Known issues
- Old `D:\work\gururmm` remote URL embedded the shared Gitea password in plaintext —
reset to a clean URL + Windows Credential Manager on rebuild.
- (Hardware) RTX 5070 Ti GSP firmware bug under sustained GPU compute — see `acg-guru-5070.md`.