Browse Source

feat(windows-ops): Refactor recover-clone onto design system

Static-mode refactor of the seventh and last windows-ops script.
Live-mode (spinner-driven progress while robocopy runs) remains
deferred — robocopy emits its own native progress so wrapping it
adds little value; the win is panel-formatting the preflight and
results.

Layout:
- Pre-flight panel before robocopy runs: source/destination/size/
  free-space summary, robocopy invocation details (retries, mirror
  mode, threads, log path), DRY-RUN alert when applicable, footer
  '• starting' (pending).
- Robocopy native output unwrapped in between (its own progress
  goes through its TEE'd console + log, no editorialising).
- Results panel after robocopy returns: verdict state (PASS/WARN/
  FAILING based on robocopy exit-code bitmask), elapsed time,
  failed-reads count, log paths, recovery hint pointing at
  ddrescue when files failed to read.

Dogfooded with -DryRun against a small test source (5 files in
windows-ops/references/) → renders preflight panel, robocopy /L
enumerates without copying, results panel shows PASS · complete.

All 7 windows-ops scripts now on the design system. JSON paths
in every script untouched. _lib/term.ps1 is the single point of
contact for chrome rendering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0xDarkMatter 2 weeks ago
parent
commit
43faa325fb
1 changed files with 67 additions and 21 deletions
  1. 67 21
      skills/windows-ops/scripts/recover-clone.ps1

+ 67 - 21
skills/windows-ops/scripts/recover-clone.ps1

@@ -81,6 +81,8 @@ param(
 
 $ErrorActionPreference = 'Stop'
 . "$PSScriptRoot\_lib\common.ps1"
+. (Join-Path $PSScriptRoot '..\..\_lib\term.ps1')
+Initialize-Term
 
 # Preflight
 $robo = Get-Command robocopy.exe -ErrorAction SilentlyContinue
@@ -102,13 +104,11 @@ try {
 
 $destDriveLetter = $Destination.Substring(0, 1).ToUpper()
 $destDrive = Get-PSDrive -PSProvider FileSystem -Name $destDriveLetter -ErrorAction SilentlyContinue
-if ($destDrive) {
-    $destFreeGB = [math]::Round($destDrive.Free / 1GB, 1)
-    Write-Log -Level INFO -Message "Source data: $srcUsedGB GB  |  Destination free: $destFreeGB GB"
-    if ($srcUsedGB -gt 0 -and $destFreeGB -lt $srcUsedGB) {
-        Write-Log -Level FAIL -Message "Destination has $destFreeGB GB free; source is $srcUsedGB GB. Insufficient space."
-        exit $script:EXIT_VALIDATION
-    }
+$destFreeGB = if ($destDrive) { [math]::Round($destDrive.Free / 1GB, 1) } else { -1 }
+
+if ($srcUsedGB -gt 0 -and $destFreeGB -gt 0 -and $destFreeGB -lt $srcUsedGB) {
+    Write-Log -Level FAIL -Message "Destination has $destFreeGB GB free; source is $srcUsedGB GB. Insufficient space."
+    exit $script:EXIT_VALIDATION
 }
 
 # Timestamps and log paths
@@ -139,15 +139,32 @@ if ($DryRun) {
     Write-Log -Level INFO -Message "DRY-RUN — robocopy /L will enumerate without copying"
 }
 
-Write-Log -Level INFO -Message "Logs:  $cloneLog"
-Write-Log -Level INFO -Message "Robocopy: robocopy $($roboArgs -join ' ')"
+# ─── Preflight panel ─────────────────────────────────────────────────────────
+$mode = if ($DryRun) { 'dry-run' } elseif ($NoMirror) { 'copy' } else { 'mirror' }
+Write-TermLine (New-TermPanelOpen -Brand 'windows-ops' -Name 'windows-ops' -Subtitle 'recover-clone' -Indicator $mode)
+Write-TermLine (New-TermPanelVert)
+$srcDisplay = if ($srcUsedGB -gt 0) { "$srcUsedGB GB" } else { 'size unknown' }
+$dstDisplay = if ($destFreeGB -gt 0) { "$destFreeGB GB free" } else { 'free space unknown' }
+Write-TermLine (New-TermSummary -Text "$Source → $Destination · $srcDisplay · destination has $dstDisplay")
+Write-TermLine (New-TermPanelVert)
+
+Write-TermLine (New-TermSection -State 'INFO' -Label 'robocopy invocation' -Count -1)
+Write-TermLine (New-TermLeaf -Name 'retries per file' -Meta "$MaxRetries (0 = recommended for failing drives)")
+Write-TermLine (New-TermLeaf -Name 'mirror mode' -Meta $(if ($NoMirror) { '/E (subtree, no delete)' } else { '/MIR' }))
+Write-TermLine (New-TermLeaf -Name 'threads' -Meta '/MT:8')
+Write-TermLine (New-TermLeaf -Name 'log' -Meta $cloneLog -IsLast)
+if ($DryRun) {
+    Write-TermLine (New-TermAlert -Severity warning -Text 'DRY-RUN — robocopy /L enumerates without copying')
+}
+Write-TermLine (New-TermPanelVert)
+Write-TermLine (New-TermPanelClose -Hotkeys (New-TermHotkey -Key '?' -Verb 'help') -Healths (New-TermHealth -State 'pending' -Text 'starting'))
 
 if (-not $PSCmdlet.ShouldProcess("$Source -> $Destination", "robocopy clone")) {
     Write-Log -Level INFO -Message "WhatIf: would run but skipped due to -WhatIf"
     exit $script:EXIT_OK
 }
 
-# Run robocopy
+# ─── Run robocopy (its own native output goes to its TEE'd console + log) ───
 $start = Get-Date
 & robocopy.exe @roboArgs
 $roboExit = $LASTEXITCODE
@@ -162,25 +179,54 @@ $end = Get-Date
 # 16     — fatal error
 # Combinations possible (bitmask). >=8 means errors.
 $elapsed = [math]::Round(($end - $start).TotalMinutes, 1)
-Write-Log -Level INFO -Message "Elapsed: $elapsed min  |  Robocopy exit: $roboExit"
 
 # Extract failed files from log
+$failedCount = 0
 if (Test-Path $cloneLog) {
     $failedFiles = Select-String -Path $cloneLog -Pattern 'ERROR \d+ \(0x[0-9A-Fa-f]+\)' -ErrorAction SilentlyContinue
     if ($failedFiles) {
         $failedFiles | ForEach-Object { $_.Line } | Set-Content -Path $failedLog
-        Write-Log -Level WARN -Message "$($failedFiles.Count) file(s) failed to copy — see: $failedLog"
+        $failedCount = $failedFiles.Count
     }
 }
 
-# Map robocopy exit to ATP semantics
-if ($roboExit -ge 16) {
-    Write-Log -Level FAIL -Message "Fatal robocopy error — review $cloneLog"
-    exit $script:EXIT_ERROR
-} elseif ($roboExit -ge 8) {
-    Write-Log -Level WARN -Message "Some files could not be copied (drive-failure or permission). Clone is partial."
-    exit 1   # partial success per ATP
+# Determine verdict
+$verdictState = if ($roboExit -ge 16) { 'FAILING' }
+                elseif ($roboExit -ge 8) { 'WARN' }
+                else { 'PASS' }
+$verdictText = switch ($verdictState) {
+    'FAILING' { 'fatal robocopy error' }
+    'WARN'    { 'partial clone — some files unreadable' }
+    'PASS'    { 'clone complete' }
+}
+
+# ─── Results panel ───────────────────────────────────────────────────────────
+Write-TermLine ''
+Write-TermLine (New-TermPanelOpen -Brand 'windows-ops' -Name 'windows-ops' -Subtitle 'recover-clone · results' -Indicator "${elapsed} min")
+Write-TermLine (New-TermPanelVert)
+Write-TermLine (New-TermSummary -Text "$verdictText · robocopy exit $roboExit")
+Write-TermLine (New-TermPanelVert)
+
+Write-TermLine (New-TermSection -State $verdictState -Label $verdictState.ToLower() -Count -1)
+Write-TermLine (New-TermLeaf -Name 'elapsed' -Meta "$elapsed minutes")
+Write-TermLine (New-TermLeaf -Name 'failed reads' -Meta "$failedCount files")
+Write-TermLine (New-TermLeaf -Name 'clone log' -Meta $cloneLog)
+if ($failedCount -gt 0) {
+    Write-TermLine (New-TermLeaf -Name 'failed list' -Meta $failedLog -IsLast)
+    Write-TermLine (New-TermAlert -Severity warning -Text "$failedCount file(s) unreadable from source — review $failedLog and consider ddrescue for bit-level recovery")
 } else {
-    Write-Log -Level PASS -Message "Clone complete. Robocopy code $roboExit (no errors)."
-    exit $script:EXIT_OK
+    Write-TermLine (New-TermLeaf -Name 'failures' -Meta 'none' -IsLast)
+}
+Write-TermLine (New-TermPanelVert)
+
+$footerHealth = switch ($verdictState) {
+    'FAILING' { New-TermHealth -State 'critical' -Text 'fatal' }
+    'WARN'    { New-TermHealth -State 'warning' -Text "$failedCount lost" }
+    'PASS'    { New-TermHealth -State 'healthy' -Text 'complete' }
 }
+Write-TermLine (New-TermPanelClose -Hotkeys (New-TermHotkey -Key '?' -Verb 'help') -Healths $footerHealth)
+
+# Map robocopy exit to ATP semantics
+if ($roboExit -ge 16) { exit $script:EXIT_ERROR }
+elseif ($roboExit -ge 8) { exit 1 }
+else { exit $script:EXIT_OK }