fix(install): merge config instead of overwriting on opencode target
The sync path's mergeJsonConfigAtKey had incoming entries overwriting existing user entries on conflict. Reverse the spread order so user config wins, matching the install path's existing behavior. Also add merge feedback logging and a test for the sync merge path. Fixes #125 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,8 @@ export async function mergeJsonConfigAtKey(options: {
|
|||||||
const merged = {
|
const merged = {
|
||||||
...existing,
|
...existing,
|
||||||
[key]: {
|
[key]: {
|
||||||
...existingEntries,
|
|
||||||
...incoming,
|
...incoming,
|
||||||
|
...existingEntries, // existing user entries win on conflict
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,12 +58,16 @@ export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBu
|
|||||||
const openCodePaths = resolveOpenCodePaths(outputRoot)
|
const openCodePaths = resolveOpenCodePaths(outputRoot)
|
||||||
await ensureDir(openCodePaths.root)
|
await ensureDir(openCodePaths.root)
|
||||||
|
|
||||||
|
const hadExistingConfig = await pathExists(openCodePaths.configPath)
|
||||||
const backupPath = await backupFile(openCodePaths.configPath)
|
const backupPath = await backupFile(openCodePaths.configPath)
|
||||||
if (backupPath) {
|
if (backupPath) {
|
||||||
console.log(`Backed up existing config to ${backupPath}`)
|
console.log(`Backed up existing config to ${backupPath}`)
|
||||||
}
|
}
|
||||||
const merged = await mergeOpenCodeConfig(openCodePaths.configPath, bundle.config)
|
const merged = await mergeOpenCodeConfig(openCodePaths.configPath, bundle.config)
|
||||||
await writeJson(openCodePaths.configPath, merged)
|
await writeJson(openCodePaths.configPath, merged)
|
||||||
|
if (hadExistingConfig) {
|
||||||
|
console.log("Merged plugin config into existing opencode.json (user settings preserved)")
|
||||||
|
}
|
||||||
|
|
||||||
const agentsDir = openCodePaths.agentsDir
|
const agentsDir = openCodePaths.agentsDir
|
||||||
for (const agent of bundle.agents) {
|
for (const agent of bundle.agents) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { promises as fs } from "fs"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { writeOpenCodeBundle } from "../src/targets/opencode"
|
import { writeOpenCodeBundle } from "../src/targets/opencode"
|
||||||
|
import { mergeJsonConfigAtKey } from "../src/sync/json-config"
|
||||||
import type { OpenCodeBundle } from "../src/types/opencode"
|
import type { OpenCodeBundle } from "../src/types/opencode"
|
||||||
|
|
||||||
async function exists(filePath: string): Promise<boolean> {
|
async function exists(filePath: string): Promise<boolean> {
|
||||||
@@ -254,3 +255,38 @@ describe("writeOpenCodeBundle", () => {
|
|||||||
expect(backupContent).toBe("old content\n")
|
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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user