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
This commit is contained in:
Kenny
2026-01-10 21:48:36 -05:00
committed by GitHub
parent 0c127879c0
commit 1c262a65fe
3 changed files with 113 additions and 2 deletions

View File

@@ -135,6 +135,7 @@ No stupid token consumption massive subagents here. No bloat tools here.
- [MCPs](#mcps)
- [LSP](#lsp)
- [Experimental](#experimental)
- [Environment Variables](#environment-variables)
- [Author's Note](#authors-note)
- [Warnings](#warnings)
- [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
**Warning**: These features are experimental and may cause unexpected behavior. Enable only if you understand the implications.
### Environment Variables
| Variable | Description |
|----------|-------------|
| `OPENCODE_CONFIG_DIR` | Override the OpenCode configuration directory. Useful for profile isolation with tools like [OCX](https://github.com/kdcokenny/ocx) ghost mode. |
## Author's Note

View File

@@ -1,6 +1,6 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { homedir } from "node:os"
import { join } from "node:path"
import { join, resolve } from "node:path"
import {
getOpenCodeConfigDir,
getOpenCodeConfigPaths,
@@ -20,6 +20,7 @@ describe("opencode-config-dir", () => {
APPDATA: process.env.APPDATA,
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
}
})
@@ -34,6 +35,84 @@ describe("opencode-config-dir", () => {
}
})
describe("OPENCODE_CONFIG_DIR environment variable", () => {
test("returns OPENCODE_CONFIG_DIR when env var is set", () => {
// #given OPENCODE_CONFIG_DIR is set to a custom path
process.env.OPENCODE_CONFIG_DIR = "/custom/opencode/path"
Object.defineProperty(process, "platform", { value: "linux" })
// #when getOpenCodeConfigDir is called with binary="opencode"
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
// #then returns the custom path
expect(result).toBe("/custom/opencode/path")
})
test("falls back to default when env var is not set", () => {
// #given OPENCODE_CONFIG_DIR is not set, platform is Linux
delete process.env.OPENCODE_CONFIG_DIR
delete process.env.XDG_CONFIG_HOME
Object.defineProperty(process, "platform", { value: "linux" })
// #when getOpenCodeConfigDir is called with binary="opencode"
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
// #then returns default ~/.config/opencode
expect(result).toBe(join(homedir(), ".config", "opencode"))
})
test("falls back to default when env var is empty string", () => {
// #given OPENCODE_CONFIG_DIR is set to empty string
process.env.OPENCODE_CONFIG_DIR = ""
delete process.env.XDG_CONFIG_HOME
Object.defineProperty(process, "platform", { value: "linux" })
// #when getOpenCodeConfigDir is called with binary="opencode"
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
// #then returns default ~/.config/opencode
expect(result).toBe(join(homedir(), ".config", "opencode"))
})
test("falls back to default when env var is whitespace only", () => {
// #given OPENCODE_CONFIG_DIR is set to whitespace only
process.env.OPENCODE_CONFIG_DIR = " "
delete process.env.XDG_CONFIG_HOME
Object.defineProperty(process, "platform", { value: "linux" })
// #when getOpenCodeConfigDir is called with binary="opencode"
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
// #then returns default ~/.config/opencode
expect(result).toBe(join(homedir(), ".config", "opencode"))
})
test("resolves relative path to absolute path", () => {
// #given OPENCODE_CONFIG_DIR is set to a relative path
process.env.OPENCODE_CONFIG_DIR = "./my-opencode-config"
Object.defineProperty(process, "platform", { value: "linux" })
// #when getOpenCodeConfigDir is called with binary="opencode"
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
// #then returns resolved absolute path
expect(result).toBe(resolve("./my-opencode-config"))
})
test("OPENCODE_CONFIG_DIR takes priority over XDG_CONFIG_HOME", () => {
// #given both OPENCODE_CONFIG_DIR and XDG_CONFIG_HOME are set
process.env.OPENCODE_CONFIG_DIR = "/custom/opencode/path"
process.env.XDG_CONFIG_HOME = "/xdg/config"
Object.defineProperty(process, "platform", { value: "linux" })
// #when getOpenCodeConfigDir is called with binary="opencode"
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
// #then OPENCODE_CONFIG_DIR takes priority
expect(result).toBe("/custom/opencode/path")
})
})
describe("isDevBuild", () => {
test("returns false for null version", () => {
expect(isDevBuild(null)).toBe(false)
@@ -213,6 +292,7 @@ describe("opencode-config-dir", () => {
// #given no config files exist
Object.defineProperty(process, "platform", { value: "linux" })
delete process.env.XDG_CONFIG_HOME
delete process.env.OPENCODE_CONFIG_DIR
// #when detectExistingConfigDir is called
const result = detectExistingConfigDir("opencode", "1.0.200")
@@ -220,5 +300,19 @@ describe("opencode-config-dir", () => {
// #then result is either null or a valid string path
expect(result === null || typeof result === "string").toBe(true)
})
test("includes OPENCODE_CONFIG_DIR in search locations when set", () => {
// #given OPENCODE_CONFIG_DIR is set to a custom path
process.env.OPENCODE_CONFIG_DIR = "/custom/opencode/path"
Object.defineProperty(process, "platform", { value: "linux" })
delete process.env.XDG_CONFIG_HOME
// #when detectExistingConfigDir is called
const result = detectExistingConfigDir("opencode", "1.0.200")
// #then result is either null (no config file exists) or a valid string path
// The important thing is that the function doesn't throw
expect(result === null || typeof result === "string").toBe(true)
})
})
})

View File

@@ -1,6 +1,6 @@
import { existsSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
import { join, resolve } from "node:path"
export type OpenCodeBinaryType = "opencode" | "opencode-desktop"
@@ -47,6 +47,11 @@ function getTauriConfigDir(identifier: string): string {
}
function getCliConfigDir(): string {
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim()
if (envConfigDir) {
return resolve(envConfigDir)
}
if (process.platform === "win32") {
const crossPlatformDir = join(homedir(), ".config", "opencode")
const crossPlatformConfig = join(crossPlatformDir, "opencode.json")
@@ -108,6 +113,11 @@ export function getOpenCodeConfigPaths(options: OpenCodeConfigDirOptions): OpenC
export function detectExistingConfigDir(binary: OpenCodeBinaryType, version?: string | null): string | null {
const locations: string[] = []
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim()
if (envConfigDir) {
locations.push(resolve(envConfigDir))
}
if (binary === "opencode-desktop") {
const identifier = isDevBuild(version) ? TAURI_APP_IDENTIFIER_DEV : TAURI_APP_IDENTIFIER
locations.push(getTauriConfigDir(identifier))