diff --git a/src/shared/toml-object.ts b/src/shared/toml-object.ts index 0e393db2..cefa4d62 100644 --- a/src/shared/toml-object.ts +++ b/src/shared/toml-object.ts @@ -9,11 +9,16 @@ function isTomlObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } +function stripUtf8Bom(rawText: string): string { + return rawText.charCodeAt(0) === 0xfeff ? rawText.slice(1) : rawText; +} + export function parseTomlObject(rawText: string): Record { - const trimmed = rawText.trim(); + const parseText = stripUtf8Bom(rawText); + const trimmed = parseText.trim(); if (!trimmed) return {}; - const parsed = parse(rawText); + const parsed = parse(parseText); if (!isTomlObject(parsed)) { throw new Error('TOML root must be a table.'); } diff --git a/tests/unit/targets/codex-cliproxy-provider-config.test.ts b/tests/unit/targets/codex-cliproxy-provider-config.test.ts index 43dd6b28..a470399e 100644 --- a/tests/unit/targets/codex-cliproxy-provider-config.test.ts +++ b/tests/unit/targets/codex-cliproxy-provider-config.test.ts @@ -49,6 +49,19 @@ describe('codex cliproxy provider config repair', () => { expect(rawText).toContain('[model_providers.cliproxy]'); }); + it('repairs Codex config files saved with a UTF-8 BOM', async () => { + fs.mkdirSync(codexHome, { recursive: true }); + const rawText = '\ufeffmodel = "gpt-5.4"\n'; + fs.writeFileSync(configPath, rawText, 'utf8'); + + const result = await ensureCodexCliproxyProviderConfig(8317, env); + + expect(result.changed).toBe(true); + const repairedText = fs.readFileSync(configPath, 'utf8'); + expect(repairedText.startsWith('\ufeffmodel = "gpt-5.4"\n\n')).toBe(true); + expect(repairedText).toContain('[model_providers.cliproxy]'); + }); + it('repairs an existing incomplete cliproxy provider', async () => { fs.mkdirSync(codexHome, { recursive: true }); fs.writeFileSync(