Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,24 @@ export namespace Config {

export class Service extends ServiceMap.Service<Service, Interface>()("@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),
Expand Down Expand Up @@ -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())
})

Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
},
})
})
Expand Down
88 changes: 88 additions & 0 deletions packages/opencode/test/server/config.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
Loading