fix(migration): drop orphan 'lsp' config key so users see LSP moved to .opencode/lsp.json (fixes #4225)

The lsp config block was removed from OhMyOpenCodeConfigSchema when LSP tools were migrated from native plugin tools to the lsp tier-1 MCP server (packages/lsp-tools-mcp). Zod v4 strips unknown keys silently on safeParse, so a v3-era oh-my-opencode.jsonc with 'lsp': { typescript: { command: ... } } continues to live in the file unchanged while doing absolutely nothing. The user reporting #4225 saw their custom LSP servers stop working with zero indication that the configuration site moved.\n\nAdd a migrator that removes the orphan lsp key during migrateConfigFile, mirroring the existing omo_agent -> sisyphus_agent migration immediately above and the 'Removed obsolete hooks from disabled_hooks' precedent below. A single log line records the configPath and the list of dropped server keys so the user has a paper trail in oh-my-opencode.log, and needsWrite is flipped so the cleanup persists to disk (with a timestamped backup) the next time the plugin loads.\n\nReproduction (clean upstream/dev, BEFORE fix):\n  needsWrite=false\n  inMemory.lsp=<original block, kept>\n  persisted.lsp=<original block, kept>\n\nVerification (AFTER fix):\n  needsWrite=true\n  inMemory.lsp=undefined\n  persisted.lsp=undefined\n\nbun test src/shared/migration/ -> 26/26 pass (24/24 pre-existing + 2 new regression tests). bun run typecheck -> exit 0.
This commit is contained in:
MoerAI
2026-05-22 16:06:19 +09:00
parent cb205e1479
commit a7429cc223
2 changed files with 63 additions and 0 deletions
@@ -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<string, unknown> = {
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<string, unknown>
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<string, unknown> = {
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<string, Record<string, unknown>>).sisyphus.model).toBe(
"anthropic/claude-opus-4-7",
)
})
})
+18
View File
@@ -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<string, unknown>)
: []
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<string, unknown>
if ("hashline_edit" in experimental) {