From ed778e62f1e0e8621df94e5d461b20833cff33e2 Mon Sep 17 00:00:00 2001 From: alexph-dev <129837470+alexph-dev@users.noreply.github.com> Date: Thu, 16 Apr 2026 00:06:42 +0700 Subject: [PATCH] fix(converters): preserve Codex config on no-MCP install (#564) --- src/targets/codex.ts | 25 ++++++++++++++------ tests/codex-writer.test.ts | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/targets/codex.ts b/src/targets/codex.ts index 2efe623..046e256 100644 --- a/src/targets/codex.ts +++ b/src/targets/codex.ts @@ -116,12 +116,29 @@ async function readFileSafe(filePath: string): Promise { export function mergeCodexConfig(existingContent: string, mcpToml: string | null): string | null { // Strip current and previous managed blocks let stripped = existingContent + let removedManagedBlock = false for (const [start, end] of [[MANAGED_START_MARKER, MANAGED_END_MARKER], [PREV_START_MARKER, PREV_END_MARKER]]) { - stripped = stripped.replace( + const next = stripped.replace( new RegExp(`${escapeForRegex(start)}[\\s\\S]*?${escapeForRegex(end)}\\n?`, "g"), "", ) + if (next !== stripped) removedManagedBlock = true + stripped = next } + + // No MCP servers to write — only remove bounded managed blocks. Do not strip + // unmarked legacy markers here: old Codex config files may contain user + // settings after "# Generated by compound-plugin", and there is no safe + // boundary for deleting only plugin-owned TOML. + if (!mcpToml) { + if (!existingContent) return null + const legacyMarkerIndex = stripped.indexOf(LEGACY_MARKER) + if (legacyMarkerIndex !== -1) { + return stripped.slice(0, legacyMarkerIndex).trimEnd() + } + return removedManagedBlock ? stripped.trimEnd() : existingContent + } + stripped = stripped.trimEnd() // Strip from legacy markers to end of content (old formats wrote everything after the marker) @@ -133,12 +150,6 @@ export function mergeCodexConfig(existingContent: string, mcpToml: string | null } } - // No MCP servers to write — return cleaned content, or null only if there was never a file - if (!mcpToml) { - if (!existingContent) return null - return cleaned - } - const managedBlock = [ MANAGED_START_MARKER, mcpToml.trim(), diff --git a/tests/codex-writer.test.ts b/tests/codex-writer.test.ts index fce4664..69de0fa 100644 --- a/tests/codex-writer.test.ts +++ b/tests/codex-writer.test.ts @@ -524,6 +524,53 @@ describe("mergeCodexConfig", () => { expect(result).toContain("[mcp_servers.new]") }) + test("preserves unmarked legacy content when no MCP servers are incoming", () => { + const existing = [ + 'model = "gpt-5.4"', + "", + "# Generated by compound-plugin", + "", + "[projects.example]", + 'trust_level = "trusted"', + ].join("\n") + + const result = mergeCodexConfig(existing, null)! + expect(result).toContain("# Generated by compound-plugin") + expect(result).toContain("[projects.example]") + expect(result).toContain('trust_level = "trusted"') + }) + + test("strips bounded legacy MCP block when no MCP servers are incoming", () => { + const existing = [ + "[user]", + 'model = "gpt-5.4"', + "", + "# MCP servers synced from Claude Code", + "", + "[mcp_servers.old]", + 'command = "old"', + ].join("\n") + + const result = mergeCodexConfig(existing, null)! + expect(result).toContain("[user]") + expect(result).not.toContain("# MCP servers synced from Claude Code") + expect(result).not.toContain("[mcp_servers.old]") + }) + + test("returns existing content byte-for-byte when no MCP servers or managed blocks exist", () => { + const existing = [ + 'model = "gpt-5.4"', + "", + "# Generated by compound-plugin", + "", + "[projects.example]", + 'trust_level = "trusted"', + "", + ].join("\n") + + expect(mergeCodexConfig(existing, null)).toBe(existing) + }) + test("preserves user config before unmarked legacy format", () => { const existing = [ "[user]",