fix(update): preserve unmanaged client settings
This commit is contained in:
1 parent
097cec41c2
commit
3973ed48f3
3 files changed
+96
-57
No files matched your search
@@ -60,16 +60,99 @@ def filter_flashbang(path):
|
|||||||
return filter_options(path, flashbang_enabled=False)
|
return filter_options(path, flashbang_enabled=False)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_json(current, managed):
|
||||||
|
if not isinstance(current, dict) or not isinstance(managed, dict):
|
||||||
|
return managed
|
||||||
|
result = dict(current)
|
||||||
|
if 'statusLine' not in managed:
|
||||||
|
result.pop('statusLine', None)
|
||||||
|
for key, value in managed.items():
|
||||||
|
if key == 'hooks' and isinstance(value, dict) and isinstance(result.get(key), dict):
|
||||||
|
hooks = dict(result[key])
|
||||||
|
for event in set(value) | {'Stop'}:
|
||||||
|
groups = value.get(event, [])
|
||||||
|
old_groups = hooks.get(event, [])
|
||||||
|
if isinstance(groups, list) and isinstance(old_groups, list):
|
||||||
|
retained = [group for group in old_groups if not any(
|
||||||
|
re.search(r'(?:flashbang|statusline|record-compact|session-state-pointer)\.(?:ps1|sh)',
|
||||||
|
str(hook.get('command', '')) + str(hook.get('command_windows', '')), re.I)
|
||||||
|
for hook in group.get('hooks', []) if isinstance(hook, dict)
|
||||||
|
)]
|
||||||
|
merged = groups + [group for group in retained if group not in groups]
|
||||||
|
if merged:
|
||||||
|
hooks[event] = merged
|
||||||
|
else:
|
||||||
|
hooks.pop(event, None)
|
||||||
|
else:
|
||||||
|
hooks[event] = value
|
||||||
|
result[key] = hooks
|
||||||
|
elif isinstance(value, dict) and isinstance(result.get(key), dict):
|
||||||
|
result[key] = merge_json(result[key], value)
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def merge_toml(current_text, managed_text):
|
||||||
|
tomllib.loads(current_text)
|
||||||
|
tomllib.loads(managed_text)
|
||||||
|
managed_root_keys = set(tomllib.loads(managed_text))
|
||||||
|
lines = current_text.splitlines(keepends=True)
|
||||||
|
root = []
|
||||||
|
blocks = []
|
||||||
|
active = root
|
||||||
|
for line in lines:
|
||||||
|
header = re.match(r'^\s*(\[\[?.+?\]\]?)\s*(?:#.*)?$', line.rstrip('\r\n'))
|
||||||
|
if header:
|
||||||
|
active = []
|
||||||
|
blocks.append((header.group(1), active))
|
||||||
|
if active is root:
|
||||||
|
assignment = re.match(r'^\s*([A-Za-z0-9_-]+)\s*=', line)
|
||||||
|
if assignment and assignment.group(1) in managed_root_keys:
|
||||||
|
continue
|
||||||
|
active.append(line)
|
||||||
|
|
||||||
|
managed_headers = set(re.findall(r'(?m)^\s*(\[\[?.+?\]\]?)\s*(?:#.*)?$', managed_text))
|
||||||
|
extras = []
|
||||||
|
for header, block in blocks:
|
||||||
|
plugin_table = re.match(r'^\[(?:plugins|marketplaces)(?:\.|\])', header)
|
||||||
|
array_table = header.startswith('[[')
|
||||||
|
owned_hook = any(re.search(r'(?:flashbang|statusline|record-compact|session-state-pointer)\.(?:ps1|sh)', line, re.I) for line in block)
|
||||||
|
if plugin_table and header in managed_headers:
|
||||||
|
raise ValueError('Cannot merge conflicting managed plugin tables.')
|
||||||
|
if header not in managed_headers or (array_table and not owned_hook):
|
||||||
|
extras.extend(block)
|
||||||
|
|
||||||
|
result = managed_text
|
||||||
|
if root and ''.join(root).strip():
|
||||||
|
managed_lines = managed_text.splitlines(keepends=True)
|
||||||
|
split = next((i for i, line in enumerate(managed_lines) if re.match(r'^\s*\[', line)), len(managed_lines))
|
||||||
|
result = ''.join(managed_lines[:split]).rstrip() + '\n' + ''.join(root).strip() + '\n' + ''.join(managed_lines[split:])
|
||||||
|
if extras:
|
||||||
|
result = result.rstrip() + '\n\n' + ''.join(extras).lstrip()
|
||||||
|
tomllib.loads(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('action', choices=('filter',))
|
parser.add_argument('action', choices=('filter', 'merge-json', 'merge-toml'))
|
||||||
parser.add_argument('--path', type=Path, required=True)
|
parser.add_argument('--path', type=Path)
|
||||||
parser.add_argument('--flashbang', choices=('true', 'false'), required=True)
|
parser.add_argument('--current', type=Path)
|
||||||
|
parser.add_argument('--flashbang', choices=('true', 'false'))
|
||||||
parser.add_argument('--statusline', choices=('true', 'false'), default='true')
|
parser.add_argument('--statusline', choices=('true', 'false'), default='true')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
try:
|
try:
|
||||||
sys.stdout.reconfigure(encoding='utf-8')
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
if args.action == 'filter':
|
||||||
print(filter_options(args.path, args.flashbang == 'true', args.statusline == 'true'), end='')
|
print(filter_options(args.path, args.flashbang == 'true', args.statusline == 'true'), end='')
|
||||||
|
else:
|
||||||
|
if args.action == 'merge-json':
|
||||||
|
current = json.loads(args.current.read_text(encoding='utf-8-sig'))
|
||||||
|
managed = json.load(sys.stdin)
|
||||||
|
print(json.dumps(merge_json(current, managed), indent=2, ensure_ascii=False) + '\n', end='')
|
||||||
|
else:
|
||||||
|
print(merge_toml(args.current.read_text(encoding='utf-8-sig'), sys.stdin.read()), end='')
|
||||||
except (OSError, ValueError, KeyError) as error:
|
except (OSError, ValueError, KeyError) as error:
|
||||||
print(f'Install options: {error}', file=sys.stderr)
|
print(f'Install options: {error}', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -112,30 +112,14 @@ function Sync-ManagedDestination {
|
|||||||
$newBytes = [System.IO.File]::ReadAllBytes($_.FullName)
|
$newBytes = [System.IO.File]::ReadAllBytes($_.FullName)
|
||||||
}
|
}
|
||||||
if ($relative -eq 'config.toml' -and (Test-Path -LiteralPath $target)) {
|
if ($relative -eq 'config.toml' -and (Test-Path -LiteralPath $target)) {
|
||||||
$localConfig = [System.IO.File]::ReadAllText($target)
|
$content = [System.Text.Encoding]::UTF8.GetString($newBytes) | & python (Join-Path $PSScriptRoot 'install-options.py') merge-toml --current $target | Out-String
|
||||||
$pluginTables = [regex]::Matches($localConfig, '(?m)^[ \t]*\[(?:plugins|marketplaces)(?:\.|\])[\s\S]*?(?=^[ \t]*\[|\z)')
|
if ($LASTEXITCODE -ne 0) { throw 'Could not merge managed config.toml keys.' }
|
||||||
$rootConfig = ([regex]::Split($localConfig, '(?m)^[ \t]*\[', 2)[0]).TrimStart([char]0xFEFF)
|
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($content)
|
||||||
$localSettings = [regex]::Matches($rootConfig, '(?m)^[ \t]*(?:model|model_reasoning_effort)[ \t]*=.*$')
|
|
||||||
if ($localSettings.Count -gt 0) {
|
|
||||||
$content = [System.Text.Encoding]::UTF8.GetString($newBytes).TrimEnd([char[]]"`r`n")
|
|
||||||
foreach ($setting in $localSettings) {
|
|
||||||
$name = ([regex]::Match($setting.Value, '^[ \t]*([^ \t=]+)')).Groups[1].Value
|
|
||||||
$content = [regex]::Replace($content, "(?m)^[ \t]*$([regex]::Escape($name))[ \t]*=.*(?:\r?\n|$)", '')
|
|
||||||
}
|
|
||||||
$preservedSettings = ($localSettings | ForEach-Object { $_.Value.TrimEnd([char[]]"`r`n") }) -join "`n"
|
|
||||||
$firstTable = [regex]::Match($content, '(?m)^[ \t]*\[')
|
|
||||||
if ($firstTable.Success) { $content = $content.Insert($firstTable.Index, "$preservedSettings`n") }
|
|
||||||
else { $content = "$content`n$preservedSettings" }
|
|
||||||
$newBytes = [System.Text.Encoding]::UTF8.GetBytes("$content`n")
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
if ($relative -eq 'settings.json' -and (Test-Path -LiteralPath $target)) {
|
||||||
|
$finalContent = [System.Text.Encoding]::UTF8.GetString($newBytes) | & python (Join-Path $PSScriptRoot 'install-options.py') merge-json --current $target | Out-String
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Could not merge managed settings.json keys.' }
|
||||||
|
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($finalContent)
|
||||||
}
|
}
|
||||||
$newHash = Get-Sha256HashOfBytes $newBytes
|
$newHash = Get-Sha256HashOfBytes $newBytes
|
||||||
$newManifest[$relative] = $newHash
|
$newManifest[$relative] = $newHash
|
||||||
|
|||||||
+3
-31
@@ -111,43 +111,15 @@ sync_managed_destination() {
|
|||||||
new_hash=$(sha256_of_file "$source_file")
|
new_hash=$(sha256_of_file "$source_file")
|
||||||
fi
|
fi
|
||||||
if [ "$relative" = config.toml ] && [ -f "$target" ]; then
|
if [ "$relative" = config.toml ] && [ -f "$target" ]; then
|
||||||
local plugin_tables local_settings setting_name
|
content=$(printf '%s' "$content" | python3 "$(dirname -- "${BASH_SOURCE[0]}")/install-options.py" merge-toml --current "$target") || return
|
||||||
local_settings=$(awk '
|
|
||||||
NR == 1 { sub(/^\357\273\277/, "") }
|
|
||||||
/^[ \t]*\[/ { exit }
|
|
||||||
/^[ \t]*(model|model_reasoning_effort)[ \t]*=/ { print }
|
|
||||||
' "$target")
|
|
||||||
if [ -n "$local_settings" ]; then
|
|
||||||
while IFS= read -r setting; do
|
|
||||||
setting_name=${setting%%=*}
|
|
||||||
setting_name=${setting_name//[[:space:]]/}
|
|
||||||
content=$(printf '%s\n' "$content" | sed "/^[[:space:]]*${setting_name}[[:space:]]*=/d")
|
|
||||||
done <<< "$local_settings"
|
|
||||||
if printf '%s\n' "$content" | grep -qE '^[[:blank:]]*\['; then
|
|
||||||
content=$(printf '%s\n' "$content" | awk -v settings="$local_settings" '
|
|
||||||
!inserted && /^[ \t]*\[/ { print settings; inserted = 1 }
|
|
||||||
{ print }
|
|
||||||
')
|
|
||||||
else
|
|
||||||
content="$content"$'\n'"$local_settings"
|
|
||||||
fi
|
|
||||||
needs_sub=true
|
needs_sub=true
|
||||||
new_hash=$(sha256_of_string "$content")
|
new_hash=$(sha256_of_string "$content")
|
||||||
fi
|
fi
|
||||||
plugin_tables=$(awk '
|
if [ "$relative" = settings.json ] && [ -f "$target" ]; then
|
||||||
/^[ \t]*\[/ { preserve = ($0 ~ /^[ \t]*\[(plugins|marketplaces)(\.|\])/) }
|
content=$(printf '%s' "$content" | python3 "$(dirname -- "${BASH_SOURCE[0]}")/install-options.py" merge-json --current "$target") || return
|
||||||
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
|
needs_sub=true
|
||||||
new_hash=$(sha256_of_string "$content")
|
new_hash=$(sha256_of_string "$content")
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
new_manifest[$relative]=$new_hash
|
new_manifest[$relative]=$new_hash
|
||||||
|
|
||||||
exists=false
|
exists=false
|
||||||
|
|||||||
Reference in new issue
Block a user