From 56895704e49d1ff73eebbcc037ba14b50043ed02 Mon Sep 17 00:00:00 2001 From: dibstern <16970307+dibstern@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:51:56 +1100 Subject: [PATCH] fix(config): update() writes to existing config file instead of hardcoding config.json Config.update() always wrote to config.json regardless of which config file the project actually used. Projects using opencode.jsonc got a spurious config.json created alongside their real config file, and the changes were effectively lost since config.json is not a valid project config filename. Add localConfigFile() to resolve the existing project config file (opencode.jsonc > opencode.json), mirroring globalConfigFile(). Handle .jsonc files with patchJsonc() to preserve comments, matching the updateGlobal() pattern. --- packages/opencode/src/config/config.ts | 29 ++++++++++++++++++-- packages/opencode/test/config/config.test.ts | 28 ++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 2b8aa9e03010..dca20bdab292 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1318,10 +1318,33 @@ export namespace Config { return global() } + function localConfigFile() { + const candidates = ["opencode.jsonc", "opencode.json"].map((file) => path.join(Instance.directory, file)) + for (const file of candidates) { + if (existsSync(file)) return file + } + // Default to opencode.json when no project config file exists yet. + // Note: config.json is only a valid config filename in the global config + // directory, not in project directories. + return candidates[1]! + } + export async function update(config: Info) { - const filepath = path.join(Instance.directory, "config.json") - const existing = await loadFile(filepath) - await Filesystem.writeJson(filepath, mergeDeep(existing, config)) + const filepath = localConfigFile() + const before = await Filesystem.readText(filepath).catch((err: any) => { + if (err.code === "ENOENT") return "{}" + throw err + }) + + if (!filepath.endsWith(".jsonc")) { + const existing = parseConfig(before, filepath) + await Filesystem.writeJson(filepath, mergeDeep(existing, config)) + } else { + const updated = patchJsonc(before, config) + parseConfig(updated, filepath) + await Filesystem.write(filepath, updated) + } + await Instance.dispose() } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 80394fbff50c..3ce89b478e9e 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -683,12 +683,38 @@ test("updates config and writes to file", async () => { const newConfig = { model: "updated/model" } await Config.update(newConfig as any) - const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "config.json")) + const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "opencode.json")) expect(writtenConfig.model).toBe("updated/model") }, }) }) +test("update writes to existing opencode.jsonc instead of creating config.json", async () => { + await using tmp = await tmpdir() + await Filesystem.write( + path.join(tmp.path, "opencode.jsonc"), + JSON.stringify({ $schema: "https://opencode.ai/config.json", model: "anthropic/claude-sonnet-4-20250514" }), + ) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await Config.update({ permission: { read: "allow" } } as any) + + // Should update the existing opencode.jsonc + const updated = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc")) + expect(updated).toContain('"permission"') + expect(updated).toContain('"read"') + + // Should NOT create a separate config.json + const configJsonExists = await Filesystem.readText(path.join(tmp.path, "config.json")).then( + () => true, + () => false, + ) + expect(configJsonExists).toBe(false) + }, + }) +}) + test("gets config directories", async () => { await using tmp = await tmpdir() await Instance.provide({