diff --git a/src/plugin-install.ts b/src/plugin-install.ts index a30a8a69..da9fe38e 100644 --- a/src/plugin-install.ts +++ b/src/plugin-install.ts @@ -369,17 +369,23 @@ function codexBlock(): string { return `\n[mcp_servers.bili]\ncommand = ${JSON.stringify(process.execPath)}\nargs = [${JSON.stringify(path.join(selfPackageRoot(), "dist", "mcp.js"))}]\nenv = { BILI_MCP_PROXY = ${JSON.stringify(proxyOriginForInstall())} }\n`; } +function malformedCodexArgs(block: string): boolean { + return /^[ \t]*args[ \t]*=[ \t]*["']/m.test(block); +} + function codexInstall(): string { const file = codexToml(); const text = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : ""; const existing = /^[ \t]*\[mcp_servers\.bili\][ \t]*$/m.exec(text); if (existing !== null) { const block = text.slice(existing.index, text.indexOf("\n[", existing.index + 1) === -1 ? undefined : text.indexOf("\n[", existing.index + 1)); - if (block.includes(`BILI_MCP_PROXY = ${JSON.stringify(proxyOriginForInstall())}`)) return `codex: already installed (${file})`; - const refreshed = text.slice(0, existing.index) + codexBlock().replace(/^\n/, "") + text.slice(existing.index + block.length); + const canonical = codexBlock().replace(/^\n/, ""); + if (block.trimEnd() === canonical.trimEnd()) return `codex: already installed (${file})`; + const refreshed = text.slice(0, existing.index) + canonical + text.slice(existing.index + block.length); backupOnce(file); fs.writeFileSync(file, refreshed); - return `codex: refreshed proxy origin -> ${file} [mcp_servers.bili]`; + const healed = malformedCodexArgs(block) ? " (repaired args: was not an array)" : ""; + return `codex: refreshed [mcp_servers.bili] -> ${file}${healed}`; } fs.mkdirSync(path.dirname(file), { recursive: true }); backupOnce(file); diff --git a/tests/plugin-agent.test.ts b/tests/plugin-agent.test.ts index c9acee19..55be3151 100644 --- a/tests/plugin-agent.test.ts +++ b/tests/plugin-agent.test.ts @@ -807,6 +807,22 @@ test("plugin install/remove roundtrips for pi/omp/codex/opencode under a fake HO assert.doesNotMatch(tomlEdge, /mcp_servers\.bili/); assert.match(tomlEdge, /\[mcp_servers\.other\]\ncommand = "x"\n/); + // #638: a malformed block (args as string - legal TOML, invalid codex + // schema) with a matching origin must NOT short-circuit to "already + // installed"; reinstall must self-heal it to the canonical block. + const selfRoot = path.dirname(path.dirname(path.resolve("src/plugin-install.ts"))); + const malformed = `[mcp_servers.other]\ncommand = "x"\n[mcp_servers.bili]\ncommand = "node"\nargs = '[\"${path.join(selfRoot, "dist", "mcp.js")}\"]'\nenv = { BILI_MCP_PROXY = "http://127.0.0.1:8787" }\n`; + fs.writeFileSync(path.join(home, "config.toml"), malformed); + const healedMsg = pluginInstall("codex"); + assert.match(healedMsg, /repaired args: was not an array/); + const tomlHealed = fs.readFileSync(path.join(home, "config.toml"), "utf8"); + assert.match(tomlHealed, /args = \[/); + assert.doesNotMatch(tomlHealed, /args = '\[/); + assert.match(tomlHealed, /\[mcp_servers\.other\]\ncommand = "x"\n/); + // A now-canonical block stays "already installed" on rerun. + assert.match(pluginInstall("codex"), /already installed/); + assert.match(pluginRemove("codex"), /removed/); + assert.match(pluginInstall("opencode"), /installed/); const oc = JSON.parse(fs.readFileSync(path.join(home, ".config/opencode/opencode.json"), "utf8")) as { mcp: Record }> }; assert.equal(oc.mcp.bili.command[1]!.endsWith(path.join("dist", "mcp.js")), true);