From 508e246a7d2d29a08c2b84b0753c224017172fe2 Mon Sep 17 00:00:00 2001 From: Kc Balusu Date: Thu, 2 Apr 2026 15:34:52 -0700 Subject: [PATCH] Fix PATCH /config local config target --- packages/opencode/src/config/config.ts | 39 +++++++-- packages/opencode/test/config/config.test.ts | 3 +- packages/opencode/test/server/config.test.ts | 88 ++++++++++++++++++++ 3 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 packages/opencode/test/server/config.test.ts diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 850bcc28bcd9..c02b8a8344ef 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1064,6 +1064,24 @@ export namespace Config { export class Service extends ServiceMap.Service()("@opencode/Config") {} + async function localConfigFile(directory: string, worktree: string) { + for await (const file of Filesystem.up({ + targets: [".opencode/opencode.jsonc", ".opencode/opencode.json", "opencode.jsonc", "opencode.json"], + start: directory, + stop: worktree, + })) { + return file + } + + if (Flag.OPENCODE_CONFIG_DIR) { + for (const file of ConfigPaths.fileInDirectory(Flag.OPENCODE_CONFIG_DIR, "opencode")) { + if (existsSync(file)) return file + } + } + + return path.join(directory, "opencode.json") + } + function globalConfigFile() { const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) => path.join(Global.Path.config, file), @@ -1478,12 +1496,21 @@ export namespace Config { }) const update = Effect.fn("Config.update")(function* (config: Info) { - const dir = yield* InstanceState.directory - const file = path.join(dir, "config.json") - const existing = yield* loadFile(file) - yield* fs - .writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2)) - .pipe(Effect.orDie) + const { directory: dir, worktree } = yield* InstanceState.context + const file = yield* Effect.promise(() => localConfigFile(dir, worktree)) + const before = (yield* readConfigFile(file)) ?? "{}" + const input = writable(config) + + if (!file.endsWith(".jsonc")) { + const existing = parseConfig(before, file) + const merged = mergeDeep(writable(existing), input) + yield* fs.writeFileString(file, JSON.stringify(merged, null, 2)).pipe(Effect.orDie) + yield* Effect.promise(() => Instance.dispose()) + return + } + + const updated = patchJsonc(before, input) + yield* fs.writeFileString(file, updated).pipe(Effect.orDie) yield* Effect.promise(() => Instance.dispose()) }) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 9c631360b620..7e9c5dd1cc65 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -710,8 +710,9 @@ 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") + expect(await Filesystem.exists(path.join(tmp.path, "config.json"))).toBeFalse() }, }) }) diff --git a/packages/opencode/test/server/config.test.ts b/packages/opencode/test/server/config.test.ts new file mode 100644 index 000000000000..e7eae0a58ad0 --- /dev/null +++ b/packages/opencode/test/server/config.test.ts @@ -0,0 +1,88 @@ +import { afterEach, describe, expect, test } from "bun:test" +import fs from "fs/promises" +import path from "path" +import { Instance } from "../../src/project/instance" +import { Server } from "../../src/server/server" +import { Log } from "../../src/util/log" +import { tmpdir } from "../fixture/fixture" + +Log.init({ print: false }) + +afterEach(async () => { + await Instance.disposeAll() +}) + +describe("config routes", () => { + test("patch writes project config to opencode.json", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const app = Server.Default() + const res = await app.request("/config", { + method: "PATCH", + headers: { + "content-type": "application/json", + "x-opencode-directory": tmp.path, + }, + body: JSON.stringify({ + model: "test/model", + }), + }) + + expect(res.status).toBe(200) + }, + }) + + const file = path.join(tmp.path, "opencode.json") + const cfg = await Bun.file(file).json() + + expect(cfg).toMatchObject({ + model: "test/model", + }) + await expect(fs.stat(path.join(tmp.path, "config.json"))).rejects.toThrow() + }) + + test("patch updates existing .opencode config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const cfg = path.join(dir, ".opencode") + await fs.mkdir(cfg, { recursive: true }) + await Bun.write( + path.join(cfg, "opencode.json"), + JSON.stringify({ + model: "base/model", + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const app = Server.Default() + const res = await app.request("/config", { + method: "PATCH", + headers: { + "content-type": "application/json", + "x-opencode-directory": tmp.path, + }, + body: JSON.stringify({ + autoshare: true, + }), + }) + + expect(res.status).toBe(200) + }, + }) + + const cfg = await Bun.file(path.join(tmp.path, ".opencode", "opencode.json")).json() + + expect(cfg).toMatchObject({ + model: "base/model", + autoshare: true, + }) + await expect(fs.stat(path.join(tmp.path, "opencode.json"))).rejects.toThrow() + await expect(fs.stat(path.join(tmp.path, "config.json"))).rejects.toThrow() + }) +})