diff --git a/README.md b/README.md index 08fa76299f..c92948cfcf 100644 --- a/README.md +++ b/README.md @@ -280,10 +280,10 @@ To remove oh-my-opencode: ```bash # Remove user config - rm -f ~/.config/opencode/oh-my-opencode.json + rm -f ~/.config/opencode/oh-my-opencode.json ~/.config/opencode/oh-my-opencode.jsonc # Remove project config (if exists) - rm -f .opencode/oh-my-opencode.json + rm -f .opencode/oh-my-opencode.json .opencode/oh-my-opencode.jsonc ``` 3. **Verify removal** @@ -314,7 +314,7 @@ Highly opinionated, but adjustable to taste. See the full [Configuration Documentation](docs/configurations.md) for detailed information. **Quick Overview:** -- **Config Locations**: `.opencode/oh-my-opencode.json` (project) or `~/.config/opencode/oh-my-opencode.json` (user) +- **Config Locations**: `.opencode/oh-my-opencode.jsonc` or `.opencode/oh-my-opencode.json` (project), `~/.config/opencode/oh-my-opencode.jsonc` or `~/.config/opencode/oh-my-opencode.json` (user) - **JSONC Support**: Comments and trailing commas supported - **Agents**: Override models, temperatures, prompts, and permissions for any agent - **Built-in Skills**: `playwright` (browser automation), `git-master` (atomic commits) diff --git a/docs/configurations.md b/docs/configurations.md index 2fb67bd475..43f8d594a4 100644 --- a/docs/configurations.md +++ b/docs/configurations.md @@ -38,13 +38,13 @@ It asks about your providers (Claude, OpenAI, Gemini, etc.) and generates optima ## Config File Locations Config file locations (priority order): -1. `.opencode/oh-my-opencode.json` (project) -2. User config (platform-specific): +1. `.opencode/oh-my-opencode.jsonc` or `.opencode/oh-my-opencode.json` (project; prefers `.jsonc` when both exist) +2. User config (platform-specific; prefers `.jsonc` when both exist): -| Platform | User Config Path | -| --------------- | ----------------------------------------------------------------------------------------------------------- | -| **Windows** | `~/.config/opencode/oh-my-opencode.json` (preferred) or `%APPDATA%\opencode\oh-my-opencode.json` (fallback) | -| **macOS/Linux** | `~/.config/opencode/oh-my-opencode.json` | +| Platform | User Config Path | +| --------------- | --------------------------------------------------------------------------------------------------------------------------- | +| **Windows** | `~/.config/opencode/oh-my-opencode.jsonc` (preferred) or `~/.config/opencode/oh-my-opencode.json` (fallback); `%APPDATA%\opencode\oh-my-opencode.jsonc` / `%APPDATA%\opencode\oh-my-opencode.json` (fallback) | +| **macOS/Linux** | `~/.config/opencode/oh-my-opencode.jsonc` (preferred) or `~/.config/opencode/oh-my-opencode.json` (fallback) | Schema autocomplete supported: @@ -1061,9 +1061,10 @@ Don't want them? Disable via `disabled_mcps` in `~/.config/opencode/oh-my-openco OpenCode provides LSP tools for analysis. Oh My OpenCode adds refactoring tools (rename, code actions). -All OpenCode LSP configs and custom settings (from opencode.json) are supported, plus additional Oh My OpenCode-specific settings. +All OpenCode LSP configs and custom settings (from `opencode.jsonc` / `opencode.json`) are supported, plus additional Oh My OpenCode-specific settings. +For config discovery, `.jsonc` takes precedence over `.json` when both exist (applies to both `opencode.*` and `oh-my-opencode.*`). -Add LSP servers via the `lsp` option in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`: +Add LSP servers via the `lsp` option in `~/.config/opencode/oh-my-opencode.jsonc` / `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.jsonc` / `.opencode/oh-my-opencode.json`: ```json { diff --git a/src/tools/lsp/server-config-loader.test.ts b/src/tools/lsp/server-config-loader.test.ts index da51038768..1d8937ab43 100644 --- a/src/tools/lsp/server-config-loader.test.ts +++ b/src/tools/lsp/server-config-loader.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from "bun:test" -import { writeFileSync, unlinkSync } from "fs" +import { writeFileSync, unlinkSync, mkdirSync, rmSync } from "fs" import { join } from "path" import { tmpdir } from "os" -import { loadJsonFile } from "./server-config-loader" +import { loadJsonFile, getConfigPaths, getMergedServers } from "./server-config-loader" describe("loadJsonFile", () => { it("parses JSONC config files with comments correctly", () => { @@ -36,4 +36,126 @@ describe("loadJsonFile", () => { // cleanup unlinkSync(tempPath) }) -}) \ No newline at end of file + + it("discovers JSONC-only user config (oh-my-opencode.jsonc)", () => { + const originalEnv = process.env.OPENCODE_CONFIG_DIR + const tempBase = join(tmpdir(), `omo-test-user-jsonc-${Date.now()}-${Math.random().toString(36).slice(2)}`) + try { + mkdirSync(tempBase, { recursive: true }) + process.env.OPENCODE_CONFIG_DIR = tempBase + + const userJsonc = `{ + // user jsonc config + "lsp": { + "user-jsonc": { + "command": ["user-jsonc-cmd"], + "extensions": [".ujs"] + } + } +}` + const userPath = join(tempBase, "oh-my-opencode.jsonc") + writeFileSync(userPath, userJsonc, "utf-8") + + const servers = getMergedServers() + const found = servers.find(s => s.id === "user-jsonc" && s.source === "user") + expect(found !== undefined).toBe(true) + } finally { + if (originalEnv === undefined) delete process.env.OPENCODE_CONFIG_DIR + else process.env.OPENCODE_CONFIG_DIR = originalEnv + rmSync(tempBase, { recursive: true, force: true }) + } + }) + + it("discovers JSONC-only opencode config (opencode.jsonc)", () => { + const originalEnv = process.env.OPENCODE_CONFIG_DIR + const tempBase = join(tmpdir(), `omo-test-oc-jsonc-${Date.now()}-${Math.random().toString(36).slice(2)}`) + try { + mkdirSync(tempBase, { recursive: true }) + process.env.OPENCODE_CONFIG_DIR = tempBase + + const opencodeJsonc = `{ + // opencode jsonc config + "lsp": { + "opencode-jsonc": { + "command": ["opencode-jsonc-cmd"], + "extensions": [".ocjs"] + } + } +}` + const opencodePath = join(tempBase, "opencode.jsonc") + writeFileSync(opencodePath, opencodeJsonc, "utf-8") + + const servers = getMergedServers() + const found = servers.find(s => s.id === "opencode-jsonc" && s.source === "opencode") + expect(found !== undefined).toBe(true) + } finally { + if (originalEnv === undefined) delete process.env.OPENCODE_CONFIG_DIR + else process.env.OPENCODE_CONFIG_DIR = originalEnv + rmSync(tempBase, { recursive: true, force: true }) + } + }) + + it("discovers JSONC-only project config (.opencode/oh-my-opencode.jsonc)", () => { + const originalCwd = process.cwd() + const tempProject = join(tmpdir(), `omo-test-project-jsonc-${Date.now()}-${Math.random().toString(36).slice(2)}`) + try { + mkdirSync(join(tempProject, ".opencode"), { recursive: true }) + const projectJsonc = `{ + // project jsonc config + "lsp": { + "project-jsonc": { + "command": ["project-jsonc-cmd"], + "extensions": [".pjs"] + } + } +}` + const projectPath = join(tempProject, ".opencode", "oh-my-opencode.jsonc") + writeFileSync(projectPath, projectJsonc, "utf-8") + + process.chdir(tempProject) + const servers = getMergedServers() + const found = servers.find(s => s.id === "project-jsonc" && s.source === "project") + expect(found !== undefined).toBe(true) + } finally { + process.chdir(originalCwd) + rmSync(tempProject, { recursive: true, force: true }) + } + }) + + it("prefers .jsonc over .json when both exist for same config id", () => { + const originalEnv = process.env.OPENCODE_CONFIG_DIR + const tempBase = join(tmpdir(), `omo-test-precedence-${Date.now()}-${Math.random().toString(36).slice(2)}`) + try { + mkdirSync(tempBase, { recursive: true }) + process.env.OPENCODE_CONFIG_DIR = tempBase + + const jsonContent = `{ + "lsp": { + "conflict": { + "command": ["from-json"], + "extensions": [".j"] + } + } +}` + const jsoncContent = `{ + // jsonc should take precedence + "lsp": { + "conflict": { + "command": ["from-jsonc"], + "extensions": [".jc"] + } + } +}` + writeFileSync(join(tempBase, "oh-my-opencode.json"), jsonContent, "utf-8") + writeFileSync(join(tempBase, "oh-my-opencode.jsonc"), jsoncContent, "utf-8") + + const servers = getMergedServers() + const found = servers.find(s => s.id === "conflict" && s.source === "user") + expect(found?.command && Array.isArray(found.command) && found.command[0] === "from-jsonc").toBe(true) + } finally { + if (originalEnv === undefined) delete process.env.OPENCODE_CONFIG_DIR + else process.env.OPENCODE_CONFIG_DIR = originalEnv + rmSync(tempBase, { recursive: true, force: true }) + } + }) +}) diff --git a/src/tools/lsp/server-config-loader.ts b/src/tools/lsp/server-config-loader.ts index 945c710e10..7a03499c7d 100644 --- a/src/tools/lsp/server-config-loader.ts +++ b/src/tools/lsp/server-config-loader.ts @@ -4,7 +4,7 @@ import { join } from "path" import { BUILTIN_SERVERS } from "./constants" import type { ResolvedServer } from "./types" import { getOpenCodeConfigDir } from "../../shared" -import { parseJsonc } from "../../shared/jsonc-parser" +import { parseJsonc, detectConfigFile } from "../../shared/jsonc-parser" interface LspEntry { disabled?: boolean @@ -38,9 +38,9 @@ export function getConfigPaths(): { project: string; user: string; opencode: str const cwd = process.cwd() const configDir = getOpenCodeConfigDir({ binary: "opencode" }) return { - project: join(cwd, ".opencode", "oh-my-opencode.json"), - user: join(configDir, "oh-my-opencode.json"), - opencode: join(configDir, "opencode.json"), + project: detectConfigFile(join(cwd, ".opencode", "oh-my-opencode")).path, + user: detectConfigFile(join(configDir, "oh-my-opencode")).path, + opencode: detectConfigFile(join(configDir, "opencode")).path, } }