Merge pull request #1765 from COLDTURNIP/fix/load_lsp_from_jsonc

fix(config): load lsp config from jsonc configuration files
This commit is contained in:
YeonGyu-Kim
2026-02-13 18:51:50 +09:00
committed by GitHub
4 changed files with 141 additions and 18 deletions

View File

@@ -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)

View File

@@ -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
{

View File

@@ -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)
})
})
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 })
}
})
})

View File

@@ -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,
}
}