Skip to content
Open
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
35 changes: 32 additions & 3 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1347,10 +1347,39 @@ export namespace Config {
}

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 = projectConfigFile()
const before = await Filesystem.readText(filepath).catch((err: any) => {
if (err.code === "ENOENT") return "{}"
throw new JsonError({ path: filepath }, { cause: err })
})

const next = await (async () => {
if (!filepath.endsWith(".jsonc")) {
const existing = parseConfig(before, filepath)
const merged = mergeDeep(existing, config)
await Filesystem.writeJson(filepath, merged)
return merged
}

const updated = patchJsonc(before, config)
const merged = parseConfig(updated, filepath)
await Filesystem.write(filepath, updated)
return merged
})()

await Instance.dispose()
return next
}

function projectConfigFile() {
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 (standard project config file name)
return path.join(Instance.directory, "opencode.json")
}

function globalConfigFile() {
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/server/routes/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export const ConfigRoutes = lazy(() =>
validator("json", Config.Info),
async (c) => {
const config = c.req.valid("json")
await Config.update(config)
return c.json(config)
const next = await Config.update(config)
return c.json(next)
},
)
.get(
Expand Down
41 changes: 39 additions & 2 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,14 +683,51 @@ test("updates config and writes to file", async () => {
directory: tmp.path,
fn: async () => {
const newConfig = { model: "updated/model" }
await Config.update(newConfig as any)
const result = await Config.update(newConfig as any)

const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "config.json"))
// Should return merged config
expect(result.model).toBe("updated/model")

// Should write to opencode.json by default (not config.json)
const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "opencode.json"))
expect(writtenConfig.model).toBe("updated/model")
},
})
})

test("updates existing opencode.json instead of creating config.json", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
// Create an existing opencode.json file
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
model: "original/model",
username: "testuser",
})
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const newConfig = { model: "updated/model" }
const result = await Config.update(newConfig as any)

// Should return merged config with both old and new fields
expect(result.model).toBe("updated/model")
expect(result.username).toBe("testuser")

// Should update existing opencode.json (not create config.json)
const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "opencode.json"))
expect(writtenConfig.model).toBe("updated/model")
expect(writtenConfig.username).toBe("testuser") // Should preserve existing fields

// config.json should not be created
const configJsonExists = await Filesystem.exists(path.join(tmp.path, "config.json"))
expect(configJsonExists).toBe(false)
},
})
})

test("gets config directories", async () => {
await using tmp = await tmpdir()
await Instance.provide({
Expand Down
Loading