diff --git a/CHANGELOG.md b/CHANGELOG.md index 09fbad95a..db67af24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes - Indexing a project that contains only config-style files (YAML, Twig, or `.properties`) no longer misleadingly reports "No files found to index" — these files are tracked at the file level and are now counted as indexed. Thanks @luojiyin1987 (#357). +- Codex installs and uninstalls now preserve TOML array-of-table sections that appear after the CodeGraph MCP entry, so unrelated user configuration is no longer removed. ## [0.9.7] - 2026-05-28 diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index 27fcbd6e8..02ffd93e2 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -1203,6 +1203,44 @@ describe('Installer targets — TOML serializer (Codex backbone)', () => { expect(content.match(/\[\[foo\]\]/g)?.length).toBe(2); expect(content).toContain('[mcp_servers.codegraph]'); }); + + it('upsert preserves a following array-of-tables sibling [[foo]]', () => { + const existing = [ + '[mcp_servers.codegraph]', + 'command = "old-codegraph"', + 'args = ["old"]', + '', + '[[foo]]', + 'name = "a"', + '', + '[[foo]]', + 'name = "b"', + '', + ].join('\n'); + const block = buildTomlTable('mcp_servers.codegraph', { command: 'codegraph', args: ['serve'] }); + const { content, action } = upsertTomlTable(existing, 'mcp_servers.codegraph', block); + expect(action).toBe('replaced'); + expect(content.match(/\[\[foo\]\]/g)?.length).toBe(2); + expect(content).toContain('name = "a"'); + expect(content).toContain('name = "b"'); + }); + + it('removeTomlTable preserves a following array-of-tables sibling [[foo]]', () => { + const existing = [ + '[mcp_servers.codegraph]', + 'command = "codegraph"', + 'args = ["serve"]', + '', + '[[foo]]', + 'name = "keep-me"', + '', + ].join('\n'); + const { content, action } = removeTomlTable(existing, 'mcp_servers.codegraph'); + expect(action).toBe('removed'); + expect(content).toContain('[[foo]]'); + expect(content).toContain('name = "keep-me"'); + expect(content).not.toContain('mcp_servers.codegraph'); + }); }); describe('Installer — uninstallTargets sweep (codegraph uninstall)', () => { diff --git a/src/installer/targets/toml.ts b/src/installer/targets/toml.ts index 29348a7c9..98f1ea43b 100644 --- a/src/installer/targets/toml.ts +++ b/src/installer/targets/toml.ts @@ -7,13 +7,13 @@ * * Strategy: treat the file as text. Find the `[mcp_servers.codegraph]` * header line, splice it (and the lines that follow it until the next - * `[...]` header or EOF) in or out. Everything outside that block is - * preserved verbatim, byte-for-byte. + * `[...]` or `[[...]]` header or EOF) in or out. Everything outside + * that block is preserved verbatim, byte-for-byte. * * Limitations (acceptable for our narrow use): - * - Only handles top-level table headers; not array-of-tables or - * subtables nested inside `[mcp_servers]` itself (we always write - * the full dotted key `[mcp_servers.codegraph]`). + * - Only edits top-level table headers. Array-of-tables and subtables + * nested inside `[mcp_servers]` are preserved as opaque siblings + * (we always write the full dotted key `[mcp_servers.codegraph]`). * - Doesn't validate sibling TOML — if the file is malformed * elsewhere, our injection won't fix it but won't make it worse. * - Quotes string values with double quotes; escapes `\` and `"`. @@ -133,22 +133,11 @@ function findHeaderIndex(content: string, headerLine: string): number { } /** - * Find the byte index of the next top-level `[...]` table header - * (excluding array-of-tables `[[...]]`) starting from `from`, or - * return content length when none. + * Find the byte index of the next top-level `[...]` or `[[...]]` + * table header starting from `from`, or return content length when + * none. */ function findNextTableHeader(content: string, from: number): number { - // Look for "\n[" but skip "\n[[" (array of tables). - let i = from; - while (i < content.length) { - const nlIdx = content.indexOf('\n[', i); - if (nlIdx === -1) return content.length; - if (content[nlIdx + 2] === '[') { - // [[...]] — keep searching past it. - i = nlIdx + 2; - continue; - } - return nlIdx + 1; - } - return content.length; + const nlIdx = content.indexOf('\n[', from); + return nlIdx === -1 ? content.length : nlIdx + 1; }