From 907746f83ec2f63c8fb4efadda01b8c7ddf2f5f7 Mon Sep 17 00:00:00 2001 From: Kieran Klaassen Date: Fri, 23 Jan 2026 08:53:52 -0800 Subject: [PATCH] fix(opencode): use correct global config path ~/.config/opencode (#117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenCode installer was writing to ~/.opencode but OpenCode expects global configuration at ~/.config/opencode per XDG Base Directory spec. Fixes: - src/commands/install.ts: Change default output from ~/.opencode to ~/.config/opencode - src/targets/opencode.ts: Recognize "opencode" basename (not just ".opencode") for direct writes without nesting Closes #114 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- src/commands/install.ts | 4 +++- src/targets/opencode.ts | 6 +++++- tests/cli.test.ts | 12 +++++++----- tests/opencode-writer.test.ts | 25 +++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/commands/install.ts b/src/commands/install.ts index 8ef936b..bab0a4b 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -175,7 +175,9 @@ function resolveOutputRoot(value: unknown): string { const expanded = expandHome(String(value).trim()) return path.resolve(expanded) } - return path.join(os.homedir(), ".opencode") + // OpenCode global config lives at ~/.config/opencode per XDG spec + // See: https://opencode.ai/docs/config/ + return path.join(os.homedir(), ".config", "opencode") } async function resolveGitHubPluginPath(pluginName: string): Promise { diff --git a/src/targets/opencode.ts b/src/targets/opencode.ts index ee3666b..09f372a 100644 --- a/src/targets/opencode.ts +++ b/src/targets/opencode.ts @@ -28,7 +28,10 @@ export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBu } function resolveOpenCodePaths(outputRoot: string) { - if (path.basename(outputRoot) === ".opencode") { + const base = path.basename(outputRoot) + // Global install: ~/.config/opencode (basename is "opencode") + // Project install: .opencode (basename is ".opencode") + if (base === "opencode" || base === ".opencode") { return { root: outputRoot, configPath: path.join(outputRoot, "opencode.json"), @@ -38,6 +41,7 @@ function resolveOpenCodePaths(outputRoot: string) { } } + // Custom output directory - nest under .opencode subdirectory return { root: outputRoot, configPath: path.join(outputRoot, "opencode.json"), diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 18c89b9..1caf903 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -63,7 +63,7 @@ describe("CLI", () => { expect(await exists(path.join(tempRoot, ".opencode", "plugins", "converted-hooks.ts"))).toBe(true) }) - test("install defaults output to ~/.opencode", async () => { + test("install defaults output to ~/.config/opencode", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-local-default-")) const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin") @@ -95,8 +95,9 @@ describe("CLI", () => { } expect(stdout).toContain("Installed compound-engineering") - expect(await exists(path.join(tempRoot, ".opencode", "opencode.json"))).toBe(true) - expect(await exists(path.join(tempRoot, ".opencode", "agents", "repo-research-analyst.md"))).toBe(true) + // OpenCode global config lives at ~/.config/opencode per XDG spec + expect(await exists(path.join(tempRoot, ".config", "opencode", "opencode.json"))).toBe(true) + expect(await exists(path.join(tempRoot, ".config", "opencode", "agents", "repo-research-analyst.md"))).toBe(true) }) test("list returns plugins in a temp workspace", async () => { @@ -174,8 +175,9 @@ describe("CLI", () => { } expect(stdout).toContain("Installed compound-engineering") - expect(await exists(path.join(tempRoot, ".opencode", "opencode.json"))).toBe(true) - expect(await exists(path.join(tempRoot, ".opencode", "agents", "repo-research-analyst.md"))).toBe(true) + // OpenCode global config lives at ~/.config/opencode per XDG spec + expect(await exists(path.join(tempRoot, ".config", "opencode", "opencode.json"))).toBe(true) + expect(await exists(path.join(tempRoot, ".config", "opencode", "agents", "repo-research-analyst.md"))).toBe(true) }) test("convert writes OpenCode output", async () => { diff --git a/tests/opencode-writer.test.ts b/tests/opencode-writer.test.ts index 01fc765..c481520 100644 --- a/tests/opencode-writer.test.ts +++ b/tests/opencode-writer.test.ts @@ -59,4 +59,29 @@ describe("writeOpenCodeBundle", () => { expect(await exists(path.join(outputRoot, "skills", "skill-one", "SKILL.md"))).toBe(true) expect(await exists(path.join(outputRoot, ".opencode"))).toBe(false) }) + + test("writes directly into ~/.config/opencode style output root", async () => { + // Simulates the global install path: ~/.config/opencode + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "config-opencode-")) + const outputRoot = path.join(tempRoot, ".config", "opencode") + const bundle: OpenCodeBundle = { + config: { $schema: "https://opencode.ai/config.json" }, + agents: [{ name: "agent-one", content: "Agent content" }], + plugins: [], + skillDirs: [ + { + name: "skill-one", + sourceDir: path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one"), + }, + ], + } + + await writeOpenCodeBundle(outputRoot, bundle) + + // Should write directly, not nested under .opencode + expect(await exists(path.join(outputRoot, "opencode.json"))).toBe(true) + expect(await exists(path.join(outputRoot, "agents", "agent-one.md"))).toBe(true) + expect(await exists(path.join(outputRoot, "skills", "skill-one", "SKILL.md"))).toBe(true) + expect(await exists(path.join(outputRoot, ".opencode"))).toBe(false) + }) })