fix(converters): preserve user config when writing MCP servers (#479)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { backupFile, copySkillDir, ensureDir, sanitizePathName, writeText } from "../utils/files"
|
||||
import { backupFile, copySkillDir, ensureDir, sanitizePathName, writeText, writeTextSecure } from "../utils/files"
|
||||
import type { CodexBundle } from "../types/codex"
|
||||
import type { ClaudeMcpServer } from "../types/claude"
|
||||
import { transformContentForCodex } from "../utils/codex-content"
|
||||
|
||||
const MANAGED_START_MARKER = "# BEGIN Compound Engineering plugin MCP -- do not edit this block"
|
||||
const MANAGED_END_MARKER = "# END Compound Engineering plugin MCP"
|
||||
const PREV_START_MARKER = "# BEGIN compound-plugin Claude Code MCP"
|
||||
const PREV_END_MARKER = "# END compound-plugin Claude Code MCP"
|
||||
const LEGACY_MARKER = "# MCP servers synced from Claude Code"
|
||||
const UNMARKED_LEGACY_MARKER = "# Generated by compound-plugin"
|
||||
|
||||
export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): Promise<void> {
|
||||
const codexRoot = resolveCodexRoot(outputRoot)
|
||||
await ensureDir(codexRoot)
|
||||
@@ -35,14 +43,16 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle):
|
||||
}
|
||||
}
|
||||
|
||||
const config = renderCodexConfig(bundle.mcpServers)
|
||||
if (config) {
|
||||
const configPath = path.join(codexRoot, "config.toml")
|
||||
const configPath = path.join(codexRoot, "config.toml")
|
||||
const existingConfig = await readFileSafe(configPath)
|
||||
const mcpToml = renderCodexConfig(bundle.mcpServers)
|
||||
const merged = mergeCodexConfig(existingConfig, mcpToml)
|
||||
if (merged !== null) {
|
||||
const backupPath = await backupFile(configPath)
|
||||
if (backupPath) {
|
||||
console.log(`Backed up existing config to ${backupPath}`)
|
||||
}
|
||||
await writeText(configPath, config)
|
||||
await writeTextSecure(configPath, merged)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,9 +63,11 @@ function resolveCodexRoot(outputRoot: string): string {
|
||||
export function renderCodexConfig(mcpServers?: Record<string, ClaudeMcpServer>): string | null {
|
||||
if (!mcpServers || Object.keys(mcpServers).length === 0) return null
|
||||
|
||||
const lines: string[] = ["# Generated by compound-plugin", ""]
|
||||
const lines: string[] = []
|
||||
|
||||
for (const [name, server] of Object.entries(mcpServers)) {
|
||||
if (!server.command && !server.url) continue
|
||||
|
||||
const key = formatTomlKey(name)
|
||||
lines.push(`[mcp_servers.${key}]`)
|
||||
|
||||
@@ -83,7 +95,60 @@ export function renderCodexConfig(mcpServers?: Record<string, ClaudeMcpServer>):
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
return lines.length > 0 ? lines.join("\n") : null
|
||||
}
|
||||
|
||||
async function readFileSafe(filePath: string): Promise<string> {
|
||||
try {
|
||||
return await fs.readFile(filePath, "utf-8")
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw err
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeCodexConfig(existingContent: string, mcpToml: string | null): string | null {
|
||||
// Strip current and previous managed blocks
|
||||
let stripped = existingContent
|
||||
for (const [start, end] of [[MANAGED_START_MARKER, MANAGED_END_MARKER], [PREV_START_MARKER, PREV_END_MARKER]]) {
|
||||
stripped = stripped.replace(
|
||||
new RegExp(`${escapeForRegex(start)}[\\s\\S]*?${escapeForRegex(end)}\\n?`, "g"),
|
||||
"",
|
||||
)
|
||||
}
|
||||
stripped = stripped.trimEnd()
|
||||
|
||||
// Strip from legacy markers to end of content (old formats wrote everything after the marker)
|
||||
let cleaned = stripped
|
||||
for (const marker of [LEGACY_MARKER, UNMARKED_LEGACY_MARKER]) {
|
||||
const idx = cleaned.indexOf(marker)
|
||||
if (idx !== -1) {
|
||||
cleaned = cleaned.slice(0, idx).trimEnd()
|
||||
}
|
||||
}
|
||||
|
||||
// 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(),
|
||||
MANAGED_END_MARKER,
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
return cleaned
|
||||
? `${cleaned}\n\n${managedBlock}`
|
||||
: `${managedBlock}`
|
||||
}
|
||||
|
||||
function escapeForRegex(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
function formatTomlString(value: string): string {
|
||||
|
||||
Reference in New Issue
Block a user