fix(opencode): use correct global config path ~/.config/opencode (#117)

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 <noreply@anthropic.com>
This commit is contained in:
Kieran Klaassen
2026-01-23 08:53:52 -08:00
committed by GitHub
parent ab38e2ffd0
commit 907746f83e
4 changed files with 40 additions and 7 deletions

View File

@@ -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<ResolvedPluginPath> {

View File

@@ -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"),

View File

@@ -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 () => {

View File

@@ -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)
})
})