fix(install): exchange install option content through UTF-8 files
Windows PowerShell pipes re-encode text by console code page: under code page 65001 a BOM broke the config.toml merge, and otherwise non-ASCII characters became question marks. Pass managed and merged content through files, and decode piped input as utf-8-sig. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bed90fee2d
commit
857720bcfd
3 files changed
+51
-12
No files matched your search
@@ -173,18 +173,24 @@ if __name__ == '__main__':
|
||||
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('--managed', type=Path)
|
||||
parser.add_argument('--output', type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
if args.action == 'filter':
|
||||
print(filter_options(args.path, args.flashbang == 'true', args.statusline == 'true'), end='')
|
||||
result = filter_options(args.path, args.flashbang == 'true', args.statusline == 'true')
|
||||
else:
|
||||
managed = args.managed.read_text(encoding='utf-8-sig') if args.managed else sys.stdin.buffer.read().decode('utf-8-sig')
|
||||
current = args.current.read_text(encoding='utf-8-sig')
|
||||
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='')
|
||||
result = json.dumps(merge_json(json.loads(current), json.loads(managed)), indent=2, ensure_ascii=False) + '\n'
|
||||
else:
|
||||
print(merge_toml(args.current.read_text(encoding='utf-8-sig'), sys.stdin.read(), args.statusline == 'true'), end='')
|
||||
result = merge_toml(current, managed, args.statusline == 'true')
|
||||
if args.output:
|
||||
args.output.write_text(result, encoding='utf-8', newline='')
|
||||
else:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
print(result, end='')
|
||||
except (OSError, ValueError, KeyError) as error:
|
||||
print(f'Install options: {error}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -86,6 +86,26 @@ function Write-ManagedManifest {
|
||||
[System.IO.File]::WriteAllText($ManifestPath, $content, (New-Object System.Text.UTF8Encoding($false)))
|
||||
}
|
||||
|
||||
function Invoke-InstallOptions {
|
||||
param([Parameter(Mandatory=$true)][string[]]$Arguments, [byte[]]$Managed, [Parameter(Mandatory=$true)][string]$ErrorMessage)
|
||||
# Exchange content through UTF-8 files: Windows PowerShell pipes re-encode text by console code page.
|
||||
$outputPath = [System.IO.Path]::GetTempFileName()
|
||||
$managedPath = $null
|
||||
try {
|
||||
if ($null -ne $Managed) {
|
||||
$managedPath = [System.IO.Path]::GetTempFileName()
|
||||
[System.IO.File]::WriteAllBytes($managedPath, $Managed)
|
||||
$Arguments += @('--managed', $managedPath)
|
||||
}
|
||||
& python (Join-Path $PSScriptRoot 'install-options.py') @Arguments --output $outputPath
|
||||
if ($LASTEXITCODE -ne 0) { throw $ErrorMessage }
|
||||
return [System.IO.File]::ReadAllText($outputPath, (New-Object System.Text.UTF8Encoding($false)))
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $outputPath -Force -ErrorAction SilentlyContinue
|
||||
if ($managedPath) { Remove-Item -LiteralPath $managedPath -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
function Sync-ManagedDestination {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -135,8 +155,7 @@ function Sync-ManagedDestination {
|
||||
if ($filterOptions) {
|
||||
$flashbangOption = if ($FlashbangEnabled) { 'true' } else { 'false' }
|
||||
$statusLineOption = if ($StatusLineEnabled) { 'true' } else { 'false' }
|
||||
$rawContent = (& python (Join-Path $PSScriptRoot 'install-options.py') filter --path $_.FullName --flashbang $flashbangOption --statusline $statusLineOption | Out-String)
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not configure install options.' }
|
||||
$rawContent = Invoke-InstallOptions -Arguments @('filter', '--path', $_.FullName, '--flashbang', $flashbangOption, '--statusline', $statusLineOption) -ErrorMessage 'Could not configure install options.'
|
||||
}
|
||||
$needsSubstitution = $filterOptions -or $rawContent.Contains('__AI_CONFIG_ROOT__') -or $rawContent.Contains('__HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_COMMAND__') -or $rawContent.Contains('__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__')
|
||||
if ($needsSubstitution) {
|
||||
@@ -147,13 +166,11 @@ function Sync-ManagedDestination {
|
||||
}
|
||||
if ($relative -eq 'config.toml' -and (Test-Path -LiteralPath $target)) {
|
||||
$statusLineFlag = if ($StatusLineEnabled) { 'true' } else { 'false' }
|
||||
$content = [System.Text.Encoding]::UTF8.GetString($newBytes) | & python (Join-Path $PSScriptRoot 'install-options.py') merge-toml --current $target --statusline $statusLineFlag | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not merge managed config.toml keys.' }
|
||||
$content = Invoke-InstallOptions -Arguments @('merge-toml', '--current', $target, '--statusline', $statusLineFlag) -Managed $newBytes -ErrorMessage 'Could not merge managed config.toml keys.'
|
||||
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($content)
|
||||
}
|
||||
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.' }
|
||||
$finalContent = Invoke-InstallOptions -Arguments @('merge-json', '--current', $target) -Managed $newBytes -ErrorMessage 'Could not merge managed settings.json keys.'
|
||||
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($finalContent)
|
||||
}
|
||||
$newHash = Get-Sha256HashOfBytes $newBytes
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import tomllib
|
||||
import unittest
|
||||
@@ -44,6 +45,21 @@ class InstallOptionsTests(unittest.TestCase):
|
||||
self.assertEqual(result['mcp_servers'], {'demo': {'command': 'demo', 'env': {'TOKEN_NAME': 'x'}}})
|
||||
self.assertEqual(result['model_providers'], {'custom': {'name': 'Custom', 'base_url': 'https://example.invalid'}})
|
||||
|
||||
def test_merge_cli_accepts_bom_input_and_preserves_non_ascii(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
current = Path(directory) / 'config.toml'
|
||||
current.write_text('# Größe\n[mcp_servers.demo]\ncommand = "ä"\n', encoding='utf-8')
|
||||
output = Path(directory) / 'out.toml'
|
||||
script = str(ROOT / 'scripts/lib/install-options.py')
|
||||
managed = 'model = "new"\n'.encode('utf-8')
|
||||
piped = subprocess.run([sys.executable, script, 'merge-toml', '--current', str(current), '--output', str(output)], input=managed)
|
||||
self.assertEqual(piped.returncode, 0)
|
||||
self.assertEqual(tomllib.loads(output.read_text(encoding='utf-8'))['mcp_servers']['demo']['command'], 'ä')
|
||||
managed_path = Path(directory) / 'managed.toml'
|
||||
managed_path.write_bytes(managed)
|
||||
subprocess.run([sys.executable, script, 'merge-toml', '--current', str(current), '--managed', str(managed_path), '--output', str(output)], check=True)
|
||||
self.assertIn('# Größe', output.read_text(encoding='utf-8'))
|
||||
|
||||
def test_filter_preserves_other_claude_hooks_and_status(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / 'settings.json'
|
||||
|
||||
Reference in new issue
Block a user