From c749fcd792c56503d4c86613f3f19d765e496022 Mon Sep 17 00:00:00 2001 From: Julian Lechner Date: Wed, 16 Sep 2026 19:18:22 +0200 Subject: [PATCH] fix(install): preserve codex plugin registrations during sync --- scripts/lib/manifest.ps1 | 12 ++++++++++++ scripts/lib/manifest.sh | 16 ++++++++++++++++ scripts/test-managed-install.ps1 | 23 +++++++++++++++++++++++ scripts/test-managed-install.sh | 20 ++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/scripts/lib/manifest.ps1 b/scripts/lib/manifest.ps1 index 6d70eff..67d2f96 100644 --- a/scripts/lib/manifest.ps1 +++ b/scripts/lib/manifest.ps1 @@ -77,6 +77,18 @@ function Sync-ManagedDestination { } else { $newBytes = [System.IO.File]::ReadAllBytes($_.FullName) } + if ($relative -eq 'config.toml' -and (Test-Path -LiteralPath $target)) { + $localConfig = [System.IO.File]::ReadAllText($target) + $pluginTables = [regex]::Matches($localConfig, '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])[\s\S]*?(?=^[ \t]*\[|\z)') + if ($pluginTables.Count -gt 0) { + $content = [System.Text.Encoding]::UTF8.GetString($newBytes).TrimEnd([char[]]"`r`n") + if ($content -match '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])') { + throw 'Cannot overwrite local plugin configuration with generated plugin tables.' + } + $preserved = ($pluginTables | ForEach-Object { $_.Value.TrimEnd([char[]]"`r`n") }) -join "`n" + $newBytes = [System.Text.Encoding]::UTF8.GetBytes("$content`n`n$preserved`n") + } + } $newHash = Get-Sha256HashOfBytes $newBytes $newManifest[$relative] = $newHash diff --git a/scripts/lib/manifest.sh b/scripts/lib/manifest.sh index a3e51c7..85e45b3 100755 --- a/scripts/lib/manifest.sh +++ b/scripts/lib/manifest.sh @@ -78,6 +78,22 @@ sync_managed_destination() { else new_hash=$(sha256_of_file "$source_file") fi + if [ "$relative" = config.toml ] && [ -f "$target" ]; then + local plugin_tables + plugin_tables=$(awk ' + /^[ \t]*\[/ { preserve = ($0 ~ /^[ \t]*\[(plugins|marketplaces)(\.|\])/) } + preserve { print } + ' "$target") + if [ -n "$plugin_tables" ]; then + if printf '%s\n' "$content" | grep -Eq '^[[:blank:]]*\[(plugins|marketplaces)(\.|\])'; then + printf 'Cannot overwrite local plugin configuration with generated plugin tables.\n' >&2 + return 1 + fi + content="$content"$'\n\n'"$plugin_tables"$'\n' + needs_sub=true + new_hash=$(sha256_of_string "$content") + fi + fi new_manifest[$relative]=$new_hash exists=false diff --git a/scripts/test-managed-install.ps1 b/scripts/test-managed-install.ps1 index d2f7225..1712805 100644 --- a/scripts/test-managed-install.ps1 +++ b/scripts/test-managed-install.ps1 @@ -44,6 +44,29 @@ 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 '; ')" } + $configPath = Join-Path $destination 'config.toml' + $pluginState = @' + [marketplaces.ponytail] + source_type = "git" + source = "https://github.com/DietrichGebert/ponytail.git" + [plugins."ponytail@ponytail"] + enabled = true + [plugins."disabled@market"] + enabled = false +'@ + [System.IO.File]::WriteAllText((Join-Path $source 'config.toml'), "model = 'new'`n") + [System.IO.File]::WriteAllText($configPath, "model = 'old'`n$pluginState`n[other]`nvalue = true`n") + $originalConfig = [System.IO.File]::ReadAllText($configPath) + $null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-dry' -DryRun + if ([System.IO.File]::ReadAllText($configPath) -ne $originalConfig) { throw 'Dry run must preserve plugin configuration.' } + $null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins' + $mergedConfig = [System.IO.File]::ReadAllText($configPath) + if (-not $mergedConfig.Contains($pluginState)) { throw 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.' } + if (-not $mergedConfig.Contains("model = 'new'") -or $mergedConfig.Contains('[other]')) { throw 'Managed configuration must still be updated.' } + if ([System.IO.File]::ReadAllText((Join-Path $destination 'backups/plugins/config.toml')) -ne $originalConfig) { throw 'Original plugin configuration must be backed up.' } + $null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-repeat' + if ([System.IO.File]::ReadAllText($configPath) -ne $mergedConfig -or (Test-Path (Join-Path $destination 'backups/plugins-repeat'))) { throw 'Preserving plugin configuration must be idempotent.' } + $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 '; ')" } diff --git a/scripts/test-managed-install.sh b/scripts/test-managed-install.sh index 26041e9..8aea35e 100755 --- a/scripts/test-managed-install.sh +++ b/scripts/test-managed-install.sh @@ -51,6 +51,26 @@ 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; } +plugin_state=' [marketplaces.ponytail] + source_type = "git" + source = "https://github.com/DietrichGebert/ponytail.git" + [plugins."ponytail@ponytail"] + enabled = true + [plugins."disabled@market"] + enabled = false' +printf "model = 'new'\n" > "$source_dir/config.toml" +printf "model = 'old'\n%s\n[other]\nvalue = true\n" "$plugin_state" > "$destination/config.toml" +original_config=$(cat "$destination/config.toml") +sync_managed_destination "$source_dir" "$destination" plugins-dry "" "" "" true > /dev/null +[ "$(cat "$destination/config.toml")" = "$original_config" ] || { printf 'Dry run changed plugin configuration.\n' >&2; exit 1; } +sync_managed_destination "$source_dir" "$destination" plugins "" "" "" false > /dev/null +merged_config=$(cat "$destination/config.toml") +[[ "$merged_config" == *"$plugin_state"* ]] || { printf 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.\n' >&2; exit 1; } +[[ "$merged_config" == *"model = 'new'"* && "$merged_config" != *'[other]'* ]] || { printf 'Managed configuration must still be updated.\n' >&2; exit 1; } +[ "$(cat "$destination/backups/plugins/config.toml")" = "$original_config" ] || { printf 'Original plugin configuration must be backed up.\n' >&2; exit 1; } +sync_managed_destination "$source_dir" "$destination" plugins-repeat "" "" "" false > /dev/null +[ "$(cat "$destination/config.toml")" = "$merged_config" ] && [ ! -d "$destination/backups/plugins-repeat" ] || { printf 'Preserving plugin configuration must be idempotent.\n' >&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