diff --git a/src/plugin-install.ts b/src/plugin-install.ts index a30a8a6..3e52f42 100644 --- a/src/plugin-install.ts +++ b/src/plugin-install.ts @@ -374,12 +374,20 @@ function codexInstall(): string { 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 blockEnd = text.indexOf("\n[", existing.index + 1); + const block = text.slice(existing.index, blockEnd === -1 ? text.length : blockEnd); + const canonical = codexBlock().replace(/^\n/, ""); + // #638: "already installed" must mean the WHOLE block matches the + // canonical one, not just the BILI_MCP_PROXY line — a block whose + // args/command drifted (e.g. args written as a string) must be + // rewritten, or the self-heal path is a silent no-op. + if (block.replace(/\s+$/, "") === canonical.replace(/\s+$/, "")) return `codex: already installed (${file})`; + const before = text.slice(0, existing.index); + const after = text.slice(blockEnd === -1 ? text.length : blockEnd); + const spliced = blockEnd === -1 ? before + canonical : before + canonical.replace(/\n$/, "") + after; backupOnce(file); - fs.writeFileSync(file, refreshed); - return `codex: refreshed proxy origin -> ${file} [mcp_servers.bili]`; + fs.writeFileSync(file, spliced); + return `codex: refreshed [mcp_servers.bili] -> ${file}`; } fs.mkdirSync(path.dirname(file), { recursive: true }); backupOnce(file); diff --git a/tests/plugin-agent.test.ts b/tests/plugin-agent.test.ts index c9acee1..38de718 100644 --- a/tests/plugin-agent.test.ts +++ b/tests/plugin-agent.test.ts @@ -797,6 +797,18 @@ test("plugin install/remove roundtrips for pi/omp/codex/opencode under a fake HO assert.doesNotMatch(tomlAfter, /mcp_servers\.bili/); assert.match(tomlAfter, /\[mcp_servers\.other\]\ncommand = "x"\n/); + // #638 regression: a block whose env line matches the current origin + // but whose args is a STRING (not an array) must be rewritten, not + // reported as "already installed" (the old check only compared the + // BILI_MCP_PROXY line, so the self-heal path was a silent no-op). + fs.writeFileSync(path.join(home, "config.toml"), + `[mcp_servers.bili]\ncommand = "node"\nargs = '["${path.join(root, "dist", "mcp.js")}"']\nenv = { BILI_MCP_PROXY = "http://127.0.0.1:8787" }\n`); + assert.match(pluginInstall("codex"), /refreshed/); + const tomlFixed = fs.readFileSync(path.join(home, "config.toml"), "utf8"); + assert.match(tomlFixed, /args = \["/); + assert.doesNotMatch(tomlFixed, /args = '/); + assert.match(pluginInstall("codex"), /already installed/); + // Regression: a header-only [mcp_servers.bili] block as the final // line with no trailing newline must be fully removed (previously // the header line survived because the next-table search matched