diff --git a/src/sync/json-config.ts b/src/sync/json-config.ts index 180993b..547ca3b 100644 --- a/src/sync/json-config.ts +++ b/src/sync/json-config.ts @@ -18,8 +18,8 @@ export async function mergeJsonConfigAtKey(options: { const merged = { ...existing, [key]: { - ...existingEntries, ...incoming, + ...existingEntries, // existing user entries win on conflict }, } diff --git a/src/targets/opencode.ts b/src/targets/opencode.ts index e0e89ff..b4bf53e 100644 --- a/src/targets/opencode.ts +++ b/src/targets/opencode.ts @@ -58,12 +58,16 @@ export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBu const openCodePaths = resolveOpenCodePaths(outputRoot) await ensureDir(openCodePaths.root) + const hadExistingConfig = await pathExists(openCodePaths.configPath) const backupPath = await backupFile(openCodePaths.configPath) if (backupPath) { console.log(`Backed up existing config to ${backupPath}`) } const merged = await mergeOpenCodeConfig(openCodePaths.configPath, bundle.config) await writeJson(openCodePaths.configPath, merged) + if (hadExistingConfig) { + console.log("Merged plugin config into existing opencode.json (user settings preserved)") + } const agentsDir = openCodePaths.agentsDir for (const agent of bundle.agents) { diff --git a/tests/opencode-writer.test.ts b/tests/opencode-writer.test.ts index 5c02cc1..f0aa976 100644 --- a/tests/opencode-writer.test.ts +++ b/tests/opencode-writer.test.ts @@ -3,6 +3,7 @@ import { promises as fs } from "fs" import path from "path" import os from "os" import { writeOpenCodeBundle } from "../src/targets/opencode" +import { mergeJsonConfigAtKey } from "../src/sync/json-config" import type { OpenCodeBundle } from "../src/types/opencode" async function exists(filePath: string): Promise { @@ -254,3 +255,38 @@ describe("writeOpenCodeBundle", () => { expect(backupContent).toBe("old content\n") }) }) + +describe("mergeJsonConfigAtKey", () => { + test("preserves existing user entries on conflict", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "json-merge-")) + const configPath = path.join(tempDir, "opencode.json") + + // User has an existing MCP server config + const existingConfig = { + model: "my-model", + mcp: { + "user-server": { type: "local", command: ["uvx", "user-srv"] }, + }, + } + await fs.writeFile(configPath, JSON.stringify(existingConfig, null, 2)) + + // Plugin tries to add its own server and override user-server + await mergeJsonConfigAtKey({ + configPath, + key: "mcp", + incoming: { + "plugin-server": { type: "local", command: ["uvx", "plugin-srv"] }, + "user-server": { type: "local", command: ["uvx", "plugin-override"] }, + }, + }) + + const merged = JSON.parse(await fs.readFile(configPath, "utf8")) + + // User's top-level keys preserved + expect(merged.model).toBe("my-model") + // Plugin server added + expect(merged.mcp["plugin-server"]).toBeDefined() + // User's server NOT overwritten by plugin + expect(merged.mcp["user-server"].command[1]).toBe("user-srv") + }) +})