From 64c9b7e42ff329d31998ea0f7cb01df6a981dcf3 Mon Sep 17 00:00:00 2001 From: markijbema <624143+markijbema@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:36:45 +0000 Subject: [PATCH] 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> --- .changeset/warn-leftover-opencode-config.md | 5 ++ .../pages/code-with-ai/platforms/cli.md | 4 + packages/kilo-docs/source-links.md | 2 + packages/opencode/src/config/config.ts | 11 +++ .../opencode/src/kilocode/config/config.ts | 47 ++++++++++ .../test/kilocode/config/config.test.ts | 89 +++++++++++++++++++ 6 files changed, 158 insertions(+) create mode 100644 .changeset/warn-leftover-opencode-config.md diff --git a/.changeset/warn-leftover-opencode-config.md b/.changeset/warn-leftover-opencode-config.md new file mode 100644 index 00000000000..192d7955442 --- /dev/null +++ b/.changeset/warn-leftover-opencode-config.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Warn on startup when a leftover opencode config directory is found. Kilo no longer falls back to opencode configuration, so the notice points you to move `.opencode` config into a `.kilo` directory (or the global kilo config dir). diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md index d785539288e..0f2993f560f 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md @@ -339,6 +339,10 @@ The Kilo CLI is a fork of [OpenCode](https://opencode.ai) and supports the same Project-level configuration takes precedence over global settings. +{% callout type="warning" %} +**Migrating from opencode?** Kilo no longer falls back to opencode configuration stored in `.opencode` directories (such as `~/.config/opencode` or a project `./.opencode/`). To keep using it, move your global config into `~/.config/kilo/` and any project config into `./.kilo/`. +{% /callout %} + ### Key Configuration Options ```json diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index b64ddd110bb..2d88e214dee 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -128,6 +128,8 @@ - +- + - - diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index e8c672fedfc..99eb8cbbab1 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -887,6 +887,17 @@ export const layer = Layer.effect( // kilocode_change end } + // kilocode_change start - warn about leftover opencode config directories (no longer read) + warnings.push( + ...(yield* KilocodeConfig.detectOpencodeConfig({ + fs, + directory: ctx.directory, + worktree: ctx.worktree, + scanProject: !Flag.KILO_DISABLE_PROJECT_CONFIG, + })), + ) + // kilocode_change end + result.agent = result.agent || {} result.mode = result.mode || {} result.plugin = result.plugin || [] diff --git a/packages/opencode/src/kilocode/config/config.ts b/packages/opencode/src/kilocode/config/config.ts index 03f0b613cf6..2ac466e652b 100644 --- a/packages/opencode/src/kilocode/config/config.ts +++ b/packages/opencode/src/kilocode/config/config.ts @@ -436,4 +436,51 @@ export namespace KilocodeConfig { export function isConfigDir(dir: string, flagDir?: string): boolean { return dir.endsWith(".kilo") || dir.endsWith(".kilocode") || dir === flagDir } + + // ── Opencode config migration notice ───────────────────────────────── + + /** Docs page describing where Kilo reads configuration from. */ + export const CONFIG_DOCS_URL = "https://kilo.ai/docs/code-with-ai/platforms/cli" + + /** + * Detect leftover opencode config directories. Kilo used to fall back to + * opencode configuration but no longer reads `.opencode` directories, so + * surface a warning telling the user where to move it. + */ + export const detectOpencodeConfig = Effect.fn("KilocodeConfig.detectOpencodeConfig")(function* (input: { + fs: AppFileSystem.Interface + directory: string + worktree?: string + scanProject: boolean + }) { + const found: string[] = [] + + // Global opencode config dir (sibling of the kilo global config dir, e.g. ~/.config/opencode). + const globalDir = path.join(path.dirname(Global.Path.config), "opencode") + if (existsSync(globalDir)) found.push(globalDir) + + // Project `.opencode` directories, walked from the working directory up to the worktree root. + const project = input.scanProject + ? yield* input.fs + .up({ targets: [".opencode"], start: input.directory, stop: input.worktree }) + .pipe(Effect.orElseSucceed(() => [] as string[])) + : [] + for (const dir of project) { + if (existsSync(dir) && !found.includes(dir)) found.push(dir) + } + + if (found.length === 0) return [] + const suffix = found.length > 1 ? ` (and ${found.length - 1} more)` : "" + return [ + { + path: found[0], + message: + `Kilo no longer falls back to opencode configuration. ` + + `Found opencode config at ${found[0]}${suffix}. ` + + `To use it in Kilo, move it into a .kilo directory (project) or ${Global.Path.config} (global). ` + + `See ${CONFIG_DOCS_URL}`, + detail: found.length > 1 ? `Locations: ${found.join(", ")}` : undefined, + }, + ] + }) } diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index 467282a3463..6a43bf148f0 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -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 () => {