Skip to content
Merged
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
96 changes: 95 additions & 1 deletion src/shared/opencode-config-dir.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
}
})

Expand All @@ -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)
Expand Down Expand Up @@ -213,12 +292,27 @@ 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")

// #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)
})
})
})
12 changes: 11 additions & 1 deletion src/shared/opencode-config-dir.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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))
Expand Down