feat(scripts): add summary mode to managed-file sync
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f75de4eb1
commit
18b6a7e390
4 files changed
+55
-18
No files matched your search
@@ -56,13 +56,15 @@ function Sync-ManagedDestination {
|
||||
[string]$AiConfigRoot,
|
||||
[string]$ShellCommand,
|
||||
[string]$WindowsShellCommand,
|
||||
[switch]$DryRun
|
||||
[switch]$DryRun,
|
||||
[switch]$Summary
|
||||
)
|
||||
Write-Output "SOURCE $Source -> $Destination"
|
||||
$manifestPath = Get-ManagedManifestPath $Destination
|
||||
$oldManifest = Read-ManagedManifest $manifestPath
|
||||
$newManifest = @{}
|
||||
$backupRoot = Join-Path $Destination "backups/$Stamp"
|
||||
$tally = @{ Created = 0; Updated = 0; Unchanged = 0; Removed = 0; Warned = 0 }
|
||||
|
||||
Get-ChildItem $Source -File -Recurse | ForEach-Object {
|
||||
$relative = $_.FullName.Substring($Source.Length).TrimStart([char[]]@('\','/')).Replace('\','/')
|
||||
@@ -79,38 +81,42 @@ function Sync-ManagedDestination {
|
||||
$newManifest[$relative] = $newHash
|
||||
|
||||
$exists = Test-Path -LiteralPath $target
|
||||
if ($exists -and (Get-Sha256Hash $target) -eq $newHash) { Write-Output "UNCHANGED $target"; return }
|
||||
if ($exists -and (Get-Sha256Hash $target) -eq $newHash) { $tally.Unchanged++; if (-not $Summary) { Write-Output "UNCHANGED $target" }; return }
|
||||
|
||||
$action = if ($exists) { 'UPDATE' } else { 'CREATE' }
|
||||
if ($DryRun) { Write-Output "DRYRUN $action $target"; return }
|
||||
if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN $action $target" }; if ($action -eq 'CREATE') { $tally.Created++ } else { $tally.Updated++ }; return }
|
||||
if ($exists) {
|
||||
$wasManaged = $oldManifest.ContainsKey($relative)
|
||||
$backup = Join-Path $backupRoot $relative
|
||||
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
|
||||
Copy-Item -LiteralPath $target $backup -Force
|
||||
Write-Output "BACKUP $target -> $backup"
|
||||
if (-not $wasManaged) { Write-Output "WARN $target existed before this installation but was not tracked by a previous run; it was backed up before being overwritten." }
|
||||
if (-not $Summary) { Write-Output "BACKUP $target -> $backup" }
|
||||
if (-not $wasManaged) { Write-Output "WARN $target existed before this installation but was not tracked by a previous run; it was backed up before being overwritten."; $tally.Warned++ }
|
||||
}
|
||||
New-Item (Split-Path $target -Parent) -ItemType Directory -Force | Out-Null
|
||||
[System.IO.File]::WriteAllBytes($target, $newBytes)
|
||||
Write-Output "$action $target"
|
||||
if (-not $Summary) { Write-Output "$action $target" }
|
||||
if ($action -eq 'CREATE') { $tally.Created++ } else { $tally.Updated++ }
|
||||
}
|
||||
|
||||
foreach ($relative in @($oldManifest.Keys | Where-Object { -not $newManifest.ContainsKey($_) })) {
|
||||
$target = Join-Path $Destination $relative
|
||||
if (-not (Test-Path -LiteralPath $target)) { continue }
|
||||
if ($DryRun) { Write-Output "DRYRUN REMOVE $target"; continue }
|
||||
if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN REMOVE $target" }; $tally.Removed++; continue }
|
||||
$backup = Join-Path $backupRoot $relative
|
||||
New-Item (Split-Path $backup -Parent) -ItemType Directory -Force | Out-Null
|
||||
Copy-Item -LiteralPath $target $backup -Force
|
||||
Write-Output "BACKUP $target -> $backup"
|
||||
if (-not $Summary) { Write-Output "BACKUP $target -> $backup" }
|
||||
if ((Get-Sha256Hash $target) -eq $oldManifest[$relative]) {
|
||||
Remove-Item -LiteralPath $target -Force
|
||||
Write-Output "REMOVE $target"
|
||||
if (-not $Summary) { Write-Output "REMOVE $target" }
|
||||
$tally.Removed++
|
||||
} else {
|
||||
Write-Output "WARN $target was managed by a previous installation and has changed locally; it was backed up but left in place instead of being removed."
|
||||
$tally.Warned++
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $DryRun) { Write-ManagedManifest $manifestPath $newManifest }
|
||||
if ($Summary) { Write-Output ("SYNC {0}: {1} created, {2} updated, {3} unchanged, {4} removed, {5} warnings" -f $Destination, $tally.Created, $tally.Updated, $tally.Unchanged, $tally.Removed, $tally.Warned) }
|
||||
}
|
||||
+26
-9
@@ -49,9 +49,9 @@ write_managed_manifest() {
|
||||
} > "$path"
|
||||
}
|
||||
|
||||
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <windows-shell-command> <dry-run: true|false>
|
||||
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <windows-shell-command> <dry-run: true|false> [summary: true|false]
|
||||
sync_managed_destination() {
|
||||
local source=$1 destination=$2 stamp=$3 ai_config_root=$4 shell_command=$5 windows_shell_command=$6 dry_run=$7
|
||||
local source=$1 destination=$2 stamp=$3 ai_config_root=$4 shell_command=$5 windows_shell_command=$6 dry_run=$7 summary=${8:-false}
|
||||
printf 'SOURCE %s -> %s\n' "$source" "$destination"
|
||||
local manifest
|
||||
manifest=$(manifest_path "$destination")
|
||||
@@ -59,6 +59,7 @@ sync_managed_destination() {
|
||||
read_managed_manifest "$manifest" old_manifest
|
||||
declare -A new_manifest
|
||||
local backup_root="$destination/backups/$stamp"
|
||||
local created=0 updated=0 unchanged=0 removed=0 warned=0
|
||||
|
||||
while IFS= read -r -d '' source_file; do
|
||||
local relative target content needs_sub new_hash exists current_hash action backup
|
||||
@@ -84,7 +85,8 @@ sync_managed_destination() {
|
||||
if [ "$exists" = true ]; then
|
||||
current_hash=$(sha256_of_file "$target")
|
||||
if [ "$current_hash" = "$new_hash" ]; then
|
||||
printf 'UNCHANGED %s\n' "$target"
|
||||
unchanged=$((unchanged + 1))
|
||||
[ "$summary" = true ] || printf 'UNCHANGED %s\n' "$target"
|
||||
continue
|
||||
fi
|
||||
action=UPDATE
|
||||
@@ -92,15 +94,20 @@ sync_managed_destination() {
|
||||
action=CREATE
|
||||
fi
|
||||
|
||||
if [ "$dry_run" = true ]; then printf 'DRYRUN %s %s\n' "$action" "$target"; continue; fi
|
||||
if [ "$dry_run" = true ]; then
|
||||
[ "$summary" = true ] || printf 'DRYRUN %s %s\n' "$action" "$target"
|
||||
[ "$action" = CREATE ] && created=$((created + 1)) || updated=$((updated + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$exists" = true ]; then
|
||||
backup="$backup_root/$relative"
|
||||
mkdir -p "$(dirname -- "$backup")"
|
||||
cp -- "$target" "$backup"
|
||||
printf 'BACKUP %s -> %s\n' "$target" "$backup"
|
||||
[ "$summary" = true ] || printf 'BACKUP %s -> %s\n' "$target" "$backup"
|
||||
if [ -z "${old_manifest[$relative]+x}" ]; then
|
||||
printf 'WARN %s existed before this installation but was not tracked by a previous run; it was backed up before being overwritten.\n' "$target"
|
||||
warned=$((warned + 1))
|
||||
fi
|
||||
fi
|
||||
mkdir -p "$(dirname -- "$target")"
|
||||
@@ -109,7 +116,8 @@ sync_managed_destination() {
|
||||
else
|
||||
cp -- "$source_file" "$target"
|
||||
fi
|
||||
printf '%s %s\n' "$action" "$target"
|
||||
[ "$summary" = true ] || printf '%s %s\n' "$action" "$target"
|
||||
[ "$action" = CREATE ] && created=$((created + 1)) || updated=$((updated + 1))
|
||||
done < <(find "$source" -type f -print0)
|
||||
|
||||
local relative target current_hash backup
|
||||
@@ -117,19 +125,28 @@ sync_managed_destination() {
|
||||
[ -z "${new_manifest[$relative]+x}" ] || continue
|
||||
target="$destination/$relative"
|
||||
[ -f "$target" ] || continue
|
||||
if [ "$dry_run" = true ]; then printf 'DRYRUN REMOVE %s\n' "$target"; continue; fi
|
||||
if [ "$dry_run" = true ]; then
|
||||
[ "$summary" = true ] || printf 'DRYRUN REMOVE %s\n' "$target"
|
||||
removed=$((removed + 1))
|
||||
continue
|
||||
fi
|
||||
backup="$backup_root/$relative"
|
||||
mkdir -p "$(dirname -- "$backup")"
|
||||
cp -- "$target" "$backup"
|
||||
printf 'BACKUP %s -> %s\n' "$target" "$backup"
|
||||
[ "$summary" = true ] || printf 'BACKUP %s -> %s\n' "$target" "$backup"
|
||||
current_hash=$(sha256_of_file "$target")
|
||||
if [ "$current_hash" = "${old_manifest[$relative]}" ]; then
|
||||
rm -f -- "$target"
|
||||
printf 'REMOVE %s\n' "$target"
|
||||
[ "$summary" = true ] || printf 'REMOVE %s\n' "$target"
|
||||
removed=$((removed + 1))
|
||||
else
|
||||
printf 'WARN %s was managed by a previous installation and has changed locally; it was backed up but left in place instead of being removed.\n' "$target"
|
||||
warned=$((warned + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$dry_run" != true ]; then write_managed_manifest "$manifest" new_manifest; fi
|
||||
if [ "$summary" = true ]; then
|
||||
printf 'SYNC %s: %s created, %s updated, %s unchanged, %s removed, %s warnings\n' "$destination" "$created" "$updated" "$unchanged" "$removed" "$warned"
|
||||
fi
|
||||
}
|
||||
@@ -44,6 +44,10 @@ try {
|
||||
if (Compare-Object $beforeDry $afterDry) { throw 'A dry run must not change the destination at all.' }
|
||||
if (@($outDry | Where-Object { $_ -match 'DRYRUN REMOVE.*a\.md' }).Count -ne 1) { throw "Dry run should report the planned removal without performing it: $($outDry -join '; ')" }
|
||||
|
||||
$outSummary = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'summary' -Summary)
|
||||
if (@($outSummary | Where-Object { $_ -like 'CREATE*' -or $_ -like 'UPDATE*' -or $_ -like 'UNCHANGED*' -or $_ -like 'BACKUP*' }).Count -ne 0) { throw "Summary mode must not print per-file lines: $($outSummary -join '; ')" }
|
||||
if (@($outSummary | Where-Object { $_ -match '^SYNC .* unchanged' }).Count -ne 1) { throw "Summary mode must print exactly one SYNC tally line: $($outSummary -join '; ')" }
|
||||
|
||||
Write-Output 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free'
|
||||
} finally {
|
||||
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
@@ -44,4 +44,14 @@ after_dry=$(find "$destination" -type f | sort)
|
||||
[ "$before_dry" = "$after_dry" ] || { printf 'A dry run must not change the destination at all.\n' >&2; exit 1; }
|
||||
printf '%s\n' "$out_dry" | grep -q '^DRYRUN REMOVE .*a\.md$' || { printf 'Dry run should report the planned removal without performing it:\n%s\n' "$out_dry" >&2; exit 1; }
|
||||
|
||||
out_summary=$(sync_managed_destination "$source_dir" "$destination" "summary" "" "" "" false true)
|
||||
if printf '%s\n' "$out_summary" | grep -Eq '^(CREATE|UPDATE|UNCHANGED|BACKUP) '; then
|
||||
printf 'Summary mode must not print per-file lines: %s\n' "$out_summary" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(printf '%s\n' "$out_summary" | grep -c '^SYNC .* unchanged')" -ne 1 ]; then
|
||||
printf 'Summary mode must print exactly one SYNC tally line: %s\n' "$out_summary" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free\n'
|
||||
Reference in new issue
Block a user