sync: auto-sync from GURU-5070 at 2026-06-06 15:46:17

Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-06 15:46:17
This commit is contained in:
2026-06-06 15:46:22 -07:00
parent f75405506e
commit 34fa93b361
3 changed files with 210 additions and 9 deletions

View File

@@ -30,6 +30,11 @@ param(
$ErrorActionPreference = 'Stop'
$u = $env:USERPROFILE
# Decode native (git) stdout as UTF-8 so captured patch text is not mangled, and give
# us a UTF-8 (no BOM) encoding for writing patches `git apply` can actually parse.
try { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) } catch {}
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
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 }
@@ -81,6 +86,37 @@ if (Test-Path "$ClaudeToolsRoot\.claude\state") { Copy-Item "$ClaudeToolsRoot\.c
Copy-Item "$ClaudeToolsRoot\.claude\bootstrap\*.ps1" "$root\bootstrap\" -Force -ErrorAction SilentlyContinue
Copy-Item "$ClaudeToolsRoot\.claude\bootstrap\RESTORE.md" "$root\bootstrap\" -Force -ErrorAction SilentlyContinue
# --- at-risk local WIP: stashes + untracked diffs that are on NO remote ---
# Written as UTF-8 (no BOM, LF) so restore-at-risk-work.ps1 / `git apply` can parse them.
# (Earlier ad-hoc captures used PowerShell `>` redirection = UTF-16, which git apply
# rejects with "No valid patches in input" - hence the explicit byte-level write here.)
$awRoot = "$root\at-risk-work"
function Save-RepoStashes($repo,$label){
if (-not (Test-Path "$repo\.git")) { return }
$marks = @(& git -C $repo stash list --format='%gd' 2>$null)
if (-not $marks) { return }
$dir = "$awRoot\$label"; New-Item -ItemType Directory -Force -Path $dir | Out-Null
$base = (& git -C $repo rev-parse HEAD 2>$null)
[System.IO.File]::WriteAllText("$dir\BASE-COMMIT.txt", "$base`n", $Utf8NoBom)
for ($i=0; $i -lt $marks.Count; $i++) {
$files = @(& git -C $repo stash show --name-only "stash@{$i}" 2>$null)
$slug = if ($files.Count) { ([IO.Path]::GetFileNameWithoutExtension($files[0])) -replace '[^\w\-]','_' } else { "stash$i" }
$lines = @(& git -C $repo --no-pager stash show -p "stash@{$i}" 2>$null)
[System.IO.File]::WriteAllText("$dir\stash$i-$slug.patch", (($lines -join "`n") + "`n"), $Utf8NoBom)
Write-Host "[OK] at-risk stash: $label stash@{$i} -> stash$i-$slug.patch"
}
}
Save-RepoStashes "$ClaudeToolsRoot\projects\msp-tools\guru-rmm" 'guru-rmm'
Save-RepoStashes "$ClaudeToolsRoot\projects\msp-tools\guru-connect" 'guru-connect'
# untracked working diffs (e.g. tmp-*.diff) that aren't committed anywhere
$gcRepo = "$ClaudeToolsRoot\projects\msp-tools\guru-connect"
if (Test-Path $gcRepo) {
Get-ChildItem $gcRepo -Filter 'tmp-*.diff' -File -ErrorAction SilentlyContinue | ForEach-Object {
$dir = "$awRoot\guru-connect"; New-Item -ItemType Directory -Force -Path $dir | Out-Null
Copy-Item $_.FullName "$dir\$($_.Name)" -Force; Write-Host "[OK] at-risk untracked diff: guru-connect\$($_.Name)"
}
}
# --- manifests ---
$m = "$root\manifests"
$tools = 'node','npm','claude','gemini','grok','ollama','py','git','gh','jq','sops','age','cargo','rustc','code','op'

View File

@@ -15,6 +15,18 @@
Non-destructive and re-runnable. If a patch won't apply cleanly (submodule moved on),
it is reported and the .patch file is left in place for manual `git apply --3way`.
ROBUSTNESS NOTES (why this is not just `git apply <file>`):
* Patch files may have been written by PowerShell redirection (UTF-16 LE/BE w/ BOM).
`git apply` only understands UTF-8/ASCII and otherwise reports
"No valid patches in input". Get-Utf8PatchPath normalizes any encoding to a
UTF-8 (no BOM) temp copy before applying.
* git writes progress/errors to stderr; capturing that with `2>&1` while
$ErrorActionPreference='Stop' turns it into a *terminating* error (PS 5.1
NativeCommandError) that aborts the whole bootstrap. Invoke-Git captures
output without that trap and returns the real exit code.
* If the submodule still has stashes, the WIP almost certainly survived the reset.
Re-applying would create DUPLICATE stashes, so we skip and report instead.
.PARAMETER BundlePath Recovery bundle root (auto-detect F:\ then E:\).
.PARAMETER ClaudeToolsRoot Default D:\claudetools.
#>
@@ -22,6 +34,35 @@
param([string]$BundlePath,[string]$ClaudeToolsRoot='D:\claudetools')
$ErrorActionPreference='Stop'
# Read a patch regardless of encoding (UTF-16 LE/BE +/- BOM, UTF-8 +/- BOM) and return
# the path to a normalized UTF-8 (no BOM) temp copy that `git apply` can parse.
function Get-Utf8PatchPath($path){
$bytes = [System.IO.File]::ReadAllBytes($path)
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { $text = [System.Text.Encoding]::Unicode.GetString($bytes,2,$bytes.Length-2) }
elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { $text = [System.Text.Encoding]::BigEndianUnicode.GetString($bytes,2,$bytes.Length-2) }
elseif ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $text = [System.Text.Encoding]::UTF8.GetString($bytes,3,$bytes.Length-3) }
else {
# No BOM: detect UTF-16 LE without BOM by counting interleaved NUL bytes in the head.
$nul = 0; $n = [Math]::Min(64,$bytes.Length)
for ($i=0; $i -lt $n; $i++) { if ($bytes[$i] -eq 0) { $nul++ } }
if ($nul -gt 8) { $text = [System.Text.Encoding]::Unicode.GetString($bytes) }
else { $text = [System.Text.Encoding]::UTF8.GetString($bytes) }
}
$text = $text -replace "`r`n","`n" # normalize to LF so git apply is happy
$tmp = [System.IO.Path]::GetTempFileName()
[System.IO.File]::WriteAllText($tmp, $text, (New-Object System.Text.UTF8Encoding($false)))
return $tmp
}
# Run git without letting native stderr (under $ErrorActionPreference='Stop') become a
# terminating error. Returns [pscustomobject]@{ Code; Output }.
function Invoke-Git([string[]]$GitArgs){
$old = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
try { $out = (& git @GitArgs 2>&1 | Out-String); $code = $LASTEXITCODE }
finally { $ErrorActionPreference = $old }
[pscustomobject]@{ Code = $code; Output = ($out).Trim() }
}
if (-not $BundlePath) { foreach ($d in 'F:','E:','D:') { if (Test-Path "$d\claudetools-recovery\at-risk-work") { $BundlePath="$d\claudetools-recovery"; break } } }
$aw = "$BundlePath\at-risk-work"
if (-not $BundlePath -or -not (Test-Path $aw)) { Write-Host "[INFO] no at-risk-work folder found in bundle - nothing to restore"; return }
@@ -32,20 +73,29 @@ function Have-Git($repo){ Test-Path "$repo\.git" }
# ---- guru-rmm stashes ----
$rmm = "$ClaudeToolsRoot\projects\msp-tools\guru-rmm"
if ((Test-Path "$aw\guru-rmm") -and (Have-Git $rmm)) {
if (git -C $rmm status --porcelain) {
$existing = (Invoke-Git @('-C',$rmm,'stash','list')).Output
if ($existing) {
Write-Host "[SKIP] guru-rmm already has stashes (local WIP survived the reset) - not re-applying to avoid duplicates:" -ForegroundColor Yellow
Write-Host $existing
Write-Host " Bundle patches remain in $aw\guru-rmm; apply by hand if you really need them." -ForegroundColor Yellow
}
elseif ((Invoke-Git @('-C',$rmm,'status','--porcelain')).Output) {
Write-Host "[WARN] guru-rmm working tree is dirty; skipping auto-restore to avoid mixing changes. Apply patches in $aw\guru-rmm manually." -ForegroundColor Yellow
} else {
# highest N first so stash0 lands at stash@{0}
$patches = Get-ChildItem "$aw\guru-rmm" -Filter '*.patch' | Sort-Object Name -Descending
foreach ($p in $patches) {
$check = git -C $rmm apply --check --3way $p.FullName 2>&1
if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] won't apply cleanly, left for manual restore: $($p.Name) ($check)" -ForegroundColor Yellow; continue }
git -C $rmm apply --3way $p.FullName 2>$null
git -C $rmm stash push -u -m "restored WIP: $($p.BaseName)" 2>$null | Out-Null
Write-Host "[OK] re-stashed guru-rmm: $($p.BaseName)" -ForegroundColor Green
$u8 = Get-Utf8PatchPath $p.FullName
try {
$chk = Invoke-Git @('-C',$rmm,'apply','--check','--3way',$u8)
if ($chk.Code -ne 0) { Write-Host "[WARN] won't apply cleanly, left for manual restore: $($p.Name) ($($chk.Output))" -ForegroundColor Yellow; continue }
Invoke-Git @('-C',$rmm,'apply','--3way',$u8) | Out-Null
Invoke-Git @('-C',$rmm,'stash','push','-u','-m',"restored WIP: $($p.BaseName)") | Out-Null
Write-Host "[OK] re-stashed guru-rmm: $($p.BaseName)" -ForegroundColor Green
} finally { Remove-Item $u8 -Force -ErrorAction SilentlyContinue }
}
Write-Host "[INFO] guru-rmm stashes now:" -ForegroundColor Cyan
git -C $rmm stash list
Write-Host (Invoke-Git @('-C',$rmm,'stash','list')).Output
}
}
@@ -53,7 +103,11 @@ if ((Test-Path "$aw\guru-rmm") -and (Have-Git $rmm)) {
$gc = "$ClaudeToolsRoot\projects\msp-tools\guru-connect"
$diff = "$aw\guru-connect\tmp-spec018.diff"
if ((Test-Path $diff) -and (Test-Path $gc)) {
Copy-Item $diff "$gc\tmp-spec018.diff" -Force
Write-Host "[OK] guru-connect\tmp-spec018.diff restored (untracked working file - 'git apply --3way tmp-spec018.diff' to apply it)" -ForegroundColor Green
if (Test-Path "$gc\tmp-spec018.diff") {
Write-Host "[SKIP] guru-connect\tmp-spec018.diff already present in repo (survived the reset) - not overwriting." -ForegroundColor Yellow
} else {
Copy-Item $diff "$gc\tmp-spec018.diff" -Force
Write-Host "[OK] guru-connect\tmp-spec018.diff restored (untracked working file - 'git apply --3way tmp-spec018.diff' to apply it)" -ForegroundColor Green
}
}
Write-Host "[DONE] at-risk WIP restore" -ForegroundColor Cyan