Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 9 additions & 8 deletions docs/configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
{
Expand Down
128 changes: 125 additions & 3 deletions src/tools/lsp/server-config-loader.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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 })
}
})
})
8 changes: 4 additions & 4 deletions src/tools/lsp/server-config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
}

Expand Down