diff --git a/src/shared/migration/config-migration.test.ts b/src/shared/migration/config-migration.test.ts index 5c41f8435..84e2d1370 100644 --- a/src/shared/migration/config-migration.test.ts +++ b/src/shared/migration/config-migration.test.ts @@ -199,3 +199,48 @@ describe("migrateConfigFile backup skipping", () => { expect(backupFiles.length).toBe(1) }) }) + +describe("migrateConfigFile orphan lsp key", () => { + test("removes the obsolete 'lsp' key from rawConfig and from the persisted file", () => { + // given - a v3-era config with a populated lsp block that the v4 schema silently strips + const workdir = createWorkdir() + const configPath = join(workdir, "oh-my-opencode.json") + const rawConfig: Record = { + lsp: { + typescript: { command: ["typescript-language-server", "--stdio"] }, + rust: { command: ["rust-analyzer"] }, + }, + } + writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n") + + // when + const needsWrite = migrateConfigFile(configPath, rawConfig) + + // then - the in-memory config and the persisted file have both lost the lsp key + expect(needsWrite).toBe(true) + expect(rawConfig.lsp).toBeUndefined() + const persistedConfig = JSON.parse(readFileSync(configPath, "utf-8")) as Record + expect(persistedConfig.lsp).toBeUndefined() + }) + + test("leaves the config alone when no 'lsp' key is present", () => { + // given - a config that never had an lsp block + const workdir = createWorkdir() + const configPath = join(workdir, "oh-my-opencode.json") + const rawConfig: Record = { + agents: { + sisyphus: { model: "anthropic/claude-opus-4-7" }, + }, + } + writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n") + + // when + const needsWrite = migrateConfigFile(configPath, rawConfig) + + // then - no rewrite triggered by the lsp migrator, agents block untouched + expect(needsWrite).toBe(false) + expect((rawConfig.agents as Record>).sisyphus.model).toBe( + "anthropic/claude-opus-4-7", + ) + }) +}) diff --git a/src/shared/migration/config-migration.ts b/src/shared/migration/config-migration.ts index 5c0ed2d87..f5c1e957f 100644 --- a/src/shared/migration/config-migration.ts +++ b/src/shared/migration/config-migration.ts @@ -105,6 +105,24 @@ export function migrateConfigFile( needsWrite = true } + // The legacy `lsp` config key was retired when LSP moved from native plugin + // tools to the `lsp` MCP server backed by `packages/lsp-tools-mcp`. Custom + // LSP servers are now configured via `.opencode/lsp.json` (project) or + // `~/.codex/lsp-client.json` (user). The Zod schema strips unknown keys + // silently, so without this migration a stale `lsp` block lingers in the + // user's config file with no signal that it has stopped doing anything. + if (copy.lsp !== undefined) { + const droppedServers = copy.lsp && typeof copy.lsp === "object" + ? Object.keys(copy.lsp as Record) + : [] + log( + "Removed obsolete 'lsp' config key from config file. LSP servers are now configured via .opencode/lsp.json -- see docs/reference/configuration.md for the new location.", + { configPath, droppedServers }, + ) + delete copy.lsp + needsWrite = true + } + if (copy.experimental && typeof copy.experimental === "object") { const experimental = copy.experimental as Record if ("hashline_edit" in experimental) {