fix(install): replace all managed files atomically
Write config.toml, settings.json, the manifest, and every other managed file to a temporary sibling and rename it over the target, so an interrupted run cannot leave a partly written file. Previously only SKILL.md was replaced this way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3285055d80
commit
d4143ae2ac
4 files changed
+30
-26
No files matched your search
@@ -43,9 +43,9 @@ function Get-Sha256HashOfBytes {
|
||||
finally { $sha256.Dispose() }
|
||||
}
|
||||
|
||||
function Write-ManagedSkillFile {
|
||||
param([Parameter(Mandatory=$true)][string]$Path,[Parameter(Mandatory=$true)][byte[]]$Bytes)
|
||||
$temporary = Join-Path (Split-Path $Path -Parent) ('.ai-config-skill-' + [Guid]::NewGuid().ToString('N') + '.tmp')
|
||||
function Write-ManagedFileAtomically {
|
||||
param([Parameter(Mandatory=$true)][string]$Path,[Parameter(Mandatory=$true)][AllowEmptyCollection()][byte[]]$Bytes)
|
||||
$temporary = Join-Path (Split-Path $Path -Parent) ('.ai-config-write-' + [Guid]::NewGuid().ToString('N') + '.tmp')
|
||||
$backup = "$temporary.bak"
|
||||
try {
|
||||
[System.IO.File]::WriteAllBytes($temporary, $Bytes)
|
||||
@@ -83,7 +83,7 @@ function Write-ManagedManifest {
|
||||
# without BOM and with LF line endings so either implementation can read the file
|
||||
# regardless of which one wrote it.
|
||||
$content = ($lines -join "`n") + "`n"
|
||||
[System.IO.File]::WriteAllText($ManifestPath, $content, (New-Object System.Text.UTF8Encoding($false)))
|
||||
Write-ManagedFileAtomically -Path $ManifestPath -Bytes ((New-Object System.Text.UTF8Encoding($false)).GetBytes($content))
|
||||
}
|
||||
|
||||
function Invoke-InstallOptions {
|
||||
@@ -190,11 +190,7 @@ function Sync-ManagedDestination {
|
||||
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
|
||||
if ($relative -eq 'SKILL.md' -or $relative.EndsWith('/SKILL.md', [StringComparison]::Ordinal)) {
|
||||
Write-ManagedSkillFile -Path $target -Bytes $newBytes
|
||||
} else {
|
||||
[System.IO.File]::WriteAllBytes($target, $newBytes)
|
||||
}
|
||||
Write-ManagedFileAtomically -Path $target -Bytes $newBytes
|
||||
if (-not $Summary) { Write-Output "$action $target" }
|
||||
if ($action -eq 'CREATE') { $tally.Created++ } else { $tally.Updated++ }
|
||||
}
|
||||
|
||||
Executable → Regular
+23
-15
@@ -66,18 +66,32 @@ read_managed_manifest() {
|
||||
done < "$path"
|
||||
}
|
||||
|
||||
# write_managed_file <target> <content-file> [source-file]
|
||||
# Replace target atomically: write a sibling temporary file, then rename it over the target.
|
||||
write_managed_file() {
|
||||
local target=$1 content_file=$2 source_file=${3:-} temporary mode
|
||||
temporary=$(mktemp "$(dirname -- "$target")/.ai-config-write.XXXXXX") || return 1
|
||||
mode=$(printf '%o' $((0666 & ~$(umask))))
|
||||
[ -z "$source_file" ] || [ ! -x "$source_file" ] || mode=$(printf '%o' $((0777 & ~$(umask))))
|
||||
{ cat -- "$content_file" > "$temporary" && chmod "$mode" "$temporary" && mv -f -- "$temporary" "$target"; } || { rm -f -- "$temporary"; return 1; }
|
||||
}
|
||||
|
||||
# write_managed_manifest <manifest-file> <assoc-array-name>
|
||||
write_managed_manifest() {
|
||||
local path=$1
|
||||
local path=$1 content
|
||||
local -n in_ref=$2
|
||||
mkdir -p "$(dirname -- "$path")"
|
||||
content=$(mktemp) || return 1
|
||||
{
|
||||
printf 'path\tsha256\n'
|
||||
while IFS= read -r rel; do
|
||||
[ -n "$rel" ] || continue
|
||||
printf '%s\t%s\n' "$rel" "${in_ref[$rel]}"
|
||||
done < <(printf '%s\n' "${!in_ref[@]}" | sort)
|
||||
} > "$path"
|
||||
} > "$content" && write_managed_file "$path" "$content"
|
||||
local status=$?
|
||||
rm -f -- "$content"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <powershell-command> <dry-run: true|false> [summary: true|false] [flashbang] [claude-concurrency] [statusline]
|
||||
@@ -176,19 +190,13 @@ sync_managed_destination() {
|
||||
fi
|
||||
fi
|
||||
mkdir -p "$(dirname -- "$target")"
|
||||
if [[ "$relative" = SKILL.md || "$relative" = */SKILL.md ]]; then
|
||||
local temporary
|
||||
temporary=$(mktemp "$(dirname -- "$target")/.ai-config-skill.XXXXXX") || return 1
|
||||
if [ "$needs_sub" = true ]; then
|
||||
printf '%s' "$content" > "$temporary" || { rm -f -- "$temporary"; return 1; }
|
||||
else
|
||||
cp -- "$source_file" "$temporary" || { rm -f -- "$temporary"; return 1; }
|
||||
fi
|
||||
mv -f -- "$temporary" "$target" || { rm -f -- "$temporary"; return 1; }
|
||||
elif [ "$needs_sub" = true ]; then
|
||||
printf '%s' "$content" > "$target"
|
||||
if [ "$needs_sub" = true ]; then
|
||||
local rendered
|
||||
rendered=$(mktemp) || return 1
|
||||
printf '%s' "$content" > "$rendered" && write_managed_file "$target" "$rendered" "$source_file" || { rm -f -- "$rendered"; return 1; }
|
||||
rm -f -- "$rendered"
|
||||
else
|
||||
cp -- "$source_file" "$target"
|
||||
write_managed_file "$target" "$source_file" "$source_file" || return 1
|
||||
fi
|
||||
[ "$summary" = true ] || printf '%s %s\n' "$action" "$target"
|
||||
[ "$action" = CREATE ] && created=$((created + 1)) || updated=$((updated + 1))
|
||||
@@ -219,7 +227,7 @@ sync_managed_destination() {
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$dry_run" != true ]; then write_managed_manifest "$manifest" new_manifest; fi
|
||||
if [ "$dry_run" != true ]; then write_managed_manifest "$manifest" new_manifest || return 1; 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
|
||||
|
||||
@@ -111,7 +111,7 @@ try {
|
||||
$installedSkill = Join-Path $testHome '.agents/skills/example/SKILL.md'
|
||||
if ((Get-Content -LiteralPath $installedSkill -Raw) -ne 'updated managed skill') { throw 'Managed skill update did not replace the file.' }
|
||||
if ((Get-Content -LiteralPath (Join-Path $skillBackupRoot 'skill-update/example/SKILL.md') -Raw) -ne 'managed skill') { throw 'New skill backup must be outside the skill tree.' }
|
||||
if (@(Get-ChildItem -LiteralPath (Split-Path $installedSkill -Parent) -Filter '.ai-config-skill-*' -File).Count) { throw 'Managed skill update left a temporary file.' }
|
||||
if (@(Get-ChildItem -LiteralPath (Split-Path $installedSkill -Parent) -Filter '.ai-config-write-*' -File).Count) { throw 'Managed skill update left a temporary file.' }
|
||||
Remove-Item -LiteralPath (Join-Path $package 'skills/example/SKILL.md') -Force
|
||||
$null = Sync-ManagedDestination -Source $targets[0].Source -Destination $targets[0].Destination -Stamp 'skill-remove'
|
||||
if (Test-Path -LiteralPath $installedSkill) { throw 'Deselected managed skill must be removed.' }
|
||||
|
||||
@@ -128,7 +128,7 @@ for target_pair in "${targets[@]}"; do
|
||||
done
|
||||
[ "$(cat "$test_home/.agents/skills/example/SKILL.md")" = 'updated managed skill' ] || { printf 'Managed skill update did not replace the file.\n' >&2; exit 1; }
|
||||
[ "$(cat "$test_home/.agents/.ai-config-skill-backups/skill-update/example/SKILL.md")" = 'managed skill' ] || { printf 'New skill backup must be outside the skill tree.\n' >&2; exit 1; }
|
||||
[ -z "$(find "$test_home/.agents/skills/example" -maxdepth 1 -name '.ai-config-skill.*' -print -quit)" ] || { printf 'Managed skill update left a temporary file.\n' >&2; exit 1; }
|
||||
[ -z "$(find "$test_home/.agents/skills/example" -maxdepth 1 -name '.ai-config-write.*' -print -quit)" ] || { printf 'Managed skill update left a temporary file.\n' >&2; exit 1; }
|
||||
rm -f -- "$package/skills/example/SKILL.md"
|
||||
sync_managed_destination "${targets[0]%%|*}" "${targets[0]#*|}" skill-remove "" "" "" false > /dev/null
|
||||
[ ! -f "$test_home/.agents/skills/example/SKILL.md" ] && [ "$(cat "$test_home/.agents/.ai-config-skill-backups/skill-remove/example/SKILL.md")" = 'updated managed skill' ] || { printf 'Deselected skill must be removed and backed up outside the skill tree.\n' >&2; exit 1; }
|
||||
|
||||
Reference in new issue
Block a user