feat(opencode): warn users about leftover opencode configuration

Kilo no longer falls back to opencode configuration stored in `.opencode`
directories. This change adds detection for both global and project-level
opencode configuration directories and issues a warning to guide users
on how to migrate their settings to the new Kilo configuration paths.

- Implement `KilocodeConfig.detectOpencodeConfig` to identify legacy
  config locations.
- Add warning messages to the CLI configuration loading process.
- Update documentation to include migration instructions.
- Add tests to verify detection of global and project-level opencode
  configs.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
This commit is contained in:
markijbema
2026-07-08 09:36:45 +00:00
co-authored by kiloconnect[bot]
parent eefd891c62
commit 64c9b7e42f
6 changed files with 158 additions and 0 deletions
@@ -616,6 +616,95 @@ describe("linked worktree config", () => {
})
})
describe("opencode config migration notice", () => {
const warnings = () =>
Effect.runPromise(Config.Service.use((svc) => svc.warnings()).pipe(Effect.scoped, Effect.provide(layer)))
test("warns when a project .opencode directory exists", async () => {
await using globalTmp = await tmpdir()
await using tmp = await tmpdir({ git: true })
await Filesystem.write(path.join(tmp.path, ".opencode", "opencode.json"), JSON.stringify({ model: "test/legacy" }))
// Isolate the global config dir so a real ~/.config/opencode on the host cannot interfere.
const prev = Global.Path.config
;(Global.Path as { config: string }).config = path.join(globalTmp.path, "kilo")
await clear()
await disposeAllInstances()
try {
await provideTestInstance({
directory: tmp.path,
fn: async () => {
await load()
const list = await warnings()
const hit = list.find((w) => w.message.includes("opencode configuration"))
expect(hit).toBeDefined()
expect(hit!.path).toContain(".opencode")
expect(hit!.message).toContain(KilocodeConfig.CONFIG_DOCS_URL)
},
})
} finally {
;(Global.Path as { config: string }).config = prev
await clear()
await disposeAllInstances()
}
})
test("warns when a global opencode config directory exists", async () => {
await using globalTmp = await tmpdir()
await using tmp = await tmpdir({ git: true })
const opencodeDir = path.join(globalTmp.path, "opencode")
await Filesystem.write(path.join(opencodeDir, "opencode.json"), JSON.stringify({ model: "test/legacy" }))
const prev = Global.Path.config
;(Global.Path as { config: string }).config = path.join(globalTmp.path, "kilo")
await clear()
await disposeAllInstances()
try {
await provideTestInstance({
directory: tmp.path,
fn: async () => {
await load()
const list = await warnings()
const hit = list.find((w) => w.message.includes("opencode configuration"))
expect(hit).toBeDefined()
expect(hit!.path).toBe(opencodeDir)
},
})
} finally {
;(Global.Path as { config: string }).config = prev
await clear()
await disposeAllInstances()
}
})
test("stays silent when no opencode config exists", async () => {
await using globalTmp = await tmpdir()
await using tmp = await tmpdir({ git: true })
const prev = Global.Path.config
;(Global.Path as { config: string }).config = path.join(globalTmp.path, "kilo")
await clear()
await disposeAllInstances()
try {
await provideTestInstance({
directory: tmp.path,
fn: async () => {
await load()
const list = await warnings()
expect(list.find((w) => w.message.includes("opencode configuration"))).toBeUndefined()
},
})
} finally {
;(Global.Path as { config: string }).config = prev
await clear()
await disposeAllInstances()
}
})
})
describe("bash permission migration", () => {
for (const action of ["allow", "ask", "deny"] as const) {
test(`preserves string-form ${action} permission in jsonc`, async () => {