feat(scripts): add summary mode to managed-file sync

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Julian lechner committed 2026-09-16 14:24:39 +02:00
1 parent 5f75de4eb1
commit 18b6a7e390
4 files changed
+55 -18

No files matched your search

+15 -9
View File
@@ -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
View File
@@ -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
}
+4
View File
@@ -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
+10
View File
@@ -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'