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({