Skip to content

Commit 1c262a6

Browse files
authored
feat: add OPENCODE_CONFIG_DIR environment variable support (#629)
- Add env var check to getCliConfigDir() for config directory override - Update detectExistingConfigDir() to include env var path in locations - Add comprehensive tests (7 test cases) - Document in README Closes #627
1 parent 0c12787 commit 1c262a6

File tree

3 files changed

+113
-2
lines changed

3 files changed

+113
-2
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ No stupid token consumption massive subagents here. No bloat tools here.
135135
- [MCPs](#mcps)
136136
- [LSP](#lsp)
137137
- [Experimental](#experimental)
138+
- [Environment Variables](#environment-variables)
138139
- [Author's Note](#authors-note)
139140
- [Warnings](#warnings)
140141
- [Loved by professionals at](#loved-by-professionals-at)
@@ -1181,6 +1182,12 @@ Opt-in experimental features that may change or be removed in future versions. U
11811182

11821183
**Warning**: These features are experimental and may cause unexpected behavior. Enable only if you understand the implications.
11831184

1185+
### Environment Variables
1186+
1187+
| Variable | Description |
1188+
|----------|-------------|
1189+
| `OPENCODE_CONFIG_DIR` | Override the OpenCode configuration directory. Useful for profile isolation with tools like [OCX](https://github.com/kdcokenny/ocx) ghost mode. |
1190+
11841191

11851192
## Author's Note
11861193

src/shared/opencode-config-dir.test.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
22
import { homedir } from "node:os"
3-
import { join } from "node:path"
3+
import { join, resolve } from "node:path"
44
import {
55
getOpenCodeConfigDir,
66
getOpenCodeConfigPaths,
@@ -20,6 +20,7 @@ describe("opencode-config-dir", () => {
2020
APPDATA: process.env.APPDATA,
2121
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
2222
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
23+
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
2324
}
2425
})
2526

@@ -34,6 +35,84 @@ describe("opencode-config-dir", () => {
3435
}
3536
})
3637

38+
describe("OPENCODE_CONFIG_DIR environment variable", () => {
39+
test("returns OPENCODE_CONFIG_DIR when env var is set", () => {
40+
// #given OPENCODE_CONFIG_DIR is set to a custom path
41+
process.env.OPENCODE_CONFIG_DIR = "/custom/opencode/path"
42+
Object.defineProperty(process, "platform", { value: "linux" })
43+
44+
// #when getOpenCodeConfigDir is called with binary="opencode"
45+
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
46+
47+
// #then returns the custom path
48+
expect(result).toBe("/custom/opencode/path")
49+
})
50+
51+
test("falls back to default when env var is not set", () => {
52+
// #given OPENCODE_CONFIG_DIR is not set, platform is Linux
53+
delete process.env.OPENCODE_CONFIG_DIR
54+
delete process.env.XDG_CONFIG_HOME
55+
Object.defineProperty(process, "platform", { value: "linux" })
56+
57+
// #when getOpenCodeConfigDir is called with binary="opencode"
58+
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
59+
60+
// #then returns default ~/.config/opencode
61+
expect(result).toBe(join(homedir(), ".config", "opencode"))
62+
})
63+
64+
test("falls back to default when env var is empty string", () => {
65+
// #given OPENCODE_CONFIG_DIR is set to empty string
66+
process.env.OPENCODE_CONFIG_DIR = ""
67+
delete process.env.XDG_CONFIG_HOME
68+
Object.defineProperty(process, "platform", { value: "linux" })
69+
70+
// #when getOpenCodeConfigDir is called with binary="opencode"
71+
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
72+
73+
// #then returns default ~/.config/opencode
74+
expect(result).toBe(join(homedir(), ".config", "opencode"))
75+
})
76+
77+
test("falls back to default when env var is whitespace only", () => {
78+
// #given OPENCODE_CONFIG_DIR is set to whitespace only
79+
process.env.OPENCODE_CONFIG_DIR = " "
80+
delete process.env.XDG_CONFIG_HOME
81+
Object.defineProperty(process, "platform", { value: "linux" })
82+
83+
// #when getOpenCodeConfigDir is called with binary="opencode"
84+
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
85+
86+
// #then returns default ~/.config/opencode
87+
expect(result).toBe(join(homedir(), ".config", "opencode"))
88+
})
89+
90+
test("resolves relative path to absolute path", () => {
91+
// #given OPENCODE_CONFIG_DIR is set to a relative path
92+
process.env.OPENCODE_CONFIG_DIR = "./my-opencode-config"
93+
Object.defineProperty(process, "platform", { value: "linux" })
94+
95+
// #when getOpenCodeConfigDir is called with binary="opencode"
96+
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
97+
98+
// #then returns resolved absolute path
99+
expect(result).toBe(resolve("./my-opencode-config"))
100+
})
101+
102+
test("OPENCODE_CONFIG_DIR takes priority over XDG_CONFIG_HOME", () => {
103+
// #given both OPENCODE_CONFIG_DIR and XDG_CONFIG_HOME are set
104+
process.env.OPENCODE_CONFIG_DIR = "/custom/opencode/path"
105+
process.env.XDG_CONFIG_HOME = "/xdg/config"
106+
Object.defineProperty(process, "platform", { value: "linux" })
107+
108+
// #when getOpenCodeConfigDir is called with binary="opencode"
109+
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
110+
111+
// #then OPENCODE_CONFIG_DIR takes priority
112+
expect(result).toBe("/custom/opencode/path")
113+
})
114+
})
115+
37116
describe("isDevBuild", () => {
38117
test("returns false for null version", () => {
39118
expect(isDevBuild(null)).toBe(false)
@@ -213,12 +292,27 @@ describe("opencode-config-dir", () => {
213292
// #given no config files exist
214293
Object.defineProperty(process, "platform", { value: "linux" })
215294
delete process.env.XDG_CONFIG_HOME
295+
delete process.env.OPENCODE_CONFIG_DIR
216296

217297
// #when detectExistingConfigDir is called
218298
const result = detectExistingConfigDir("opencode", "1.0.200")
219299

220300
// #then result is either null or a valid string path
221301
expect(result === null || typeof result === "string").toBe(true)
222302
})
303+
304+
test("includes OPENCODE_CONFIG_DIR in search locations when set", () => {
305+
// #given OPENCODE_CONFIG_DIR is set to a custom path
306+
process.env.OPENCODE_CONFIG_DIR = "/custom/opencode/path"
307+
Object.defineProperty(process, "platform", { value: "linux" })
308+
delete process.env.XDG_CONFIG_HOME
309+
310+
// #when detectExistingConfigDir is called
311+
const result = detectExistingConfigDir("opencode", "1.0.200")
312+
313+
// #then result is either null (no config file exists) or a valid string path
314+
// The important thing is that the function doesn't throw
315+
expect(result === null || typeof result === "string").toBe(true)
316+
})
223317
})
224318
})

src/shared/opencode-config-dir.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { existsSync } from "node:fs"
22
import { homedir } from "node:os"
3-
import { join } from "node:path"
3+
import { join, resolve } from "node:path"
44

55
export type OpenCodeBinaryType = "opencode" | "opencode-desktop"
66

@@ -47,6 +47,11 @@ function getTauriConfigDir(identifier: string): string {
4747
}
4848

4949
function getCliConfigDir(): string {
50+
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim()
51+
if (envConfigDir) {
52+
return resolve(envConfigDir)
53+
}
54+
5055
if (process.platform === "win32") {
5156
const crossPlatformDir = join(homedir(), ".config", "opencode")
5257
const crossPlatformConfig = join(crossPlatformDir, "opencode.json")
@@ -108,6 +113,11 @@ export function getOpenCodeConfigPaths(options: OpenCodeConfigDirOptions): OpenC
108113
export function detectExistingConfigDir(binary: OpenCodeBinaryType, version?: string | null): string | null {
109114
const locations: string[] = []
110115

116+
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim()
117+
if (envConfigDir) {
118+
locations.push(resolve(envConfigDir))
119+
}
120+
111121
if (binary === "opencode-desktop") {
112122
const identifier = isDevBuild(version) ? TAURI_APP_IDENTIFIER_DEV : TAURI_APP_IDENTIFIER
113123
locations.push(getTauriConfigDir(identifier))

0 commit comments

Comments
 (0)