fix(shared): add additive opencode config directory discovery
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { basename, dirname, join } from "node:path"
|
import { basename, dirname, join } from "node:path"
|
||||||
import { getOpenCodeConfigDir } from "./opencode-config-dir"
|
import { getOpenCodeConfigDirs } from "./opencode-config-dir"
|
||||||
import type { OpenCodeConfigDirOptions } from "./opencode-config-dir-types"
|
import type { OpenCodeConfigDirOptions } from "./opencode-config-dir-types"
|
||||||
|
|
||||||
function getParentOpencodeConfigDir(configDir: string): string | null {
|
function getParentOpencodeConfigDir(configDir: string): string | null {
|
||||||
@@ -12,25 +12,33 @@ function getParentOpencodeConfigDir(configDir: string): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getOpenCodeCommandDirs(options: OpenCodeConfigDirOptions): string[] {
|
export function getOpenCodeCommandDirs(options: OpenCodeConfigDirOptions): string[] {
|
||||||
const configDir = getOpenCodeConfigDir(options)
|
const configDirs = getOpenCodeConfigDirs(options)
|
||||||
const parentConfigDir = getParentOpencodeConfigDir(configDir)
|
|
||||||
return Array.from(
|
return Array.from(
|
||||||
new Set([
|
new Set([
|
||||||
join(configDir, "commands"),
|
...configDirs.flatMap((configDir) => {
|
||||||
join(configDir, "command"),
|
const parentConfigDir = getParentOpencodeConfigDir(configDir)
|
||||||
...(parentConfigDir ? [join(parentConfigDir, "commands"), join(parentConfigDir, "command")] : []),
|
return [
|
||||||
|
join(configDir, "commands"),
|
||||||
|
join(configDir, "command"),
|
||||||
|
...(parentConfigDir ? [join(parentConfigDir, "commands"), join(parentConfigDir, "command")] : []),
|
||||||
|
]
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOpenCodeSkillDirs(options: OpenCodeConfigDirOptions): string[] {
|
export function getOpenCodeSkillDirs(options: OpenCodeConfigDirOptions): string[] {
|
||||||
const configDir = getOpenCodeConfigDir(options)
|
const configDirs = getOpenCodeConfigDirs(options)
|
||||||
const parentConfigDir = getParentOpencodeConfigDir(configDir)
|
|
||||||
return Array.from(
|
return Array.from(
|
||||||
new Set([
|
new Set([
|
||||||
join(configDir, "skills"),
|
...configDirs.flatMap((configDir) => {
|
||||||
join(configDir, "skill"),
|
const parentConfigDir = getParentOpencodeConfigDir(configDir)
|
||||||
...(parentConfigDir ? [join(parentConfigDir, "skills"), join(parentConfigDir, "skill")] : []),
|
return [
|
||||||
|
join(configDir, "skills"),
|
||||||
|
join(configDir, "skill"),
|
||||||
|
...(parentConfigDir ? [join(parentConfigDir, "skills"), join(parentConfigDir, "skill")] : []),
|
||||||
|
]
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { homedir } from "node:os"
|
|||||||
import { join, resolve, win32 } from "node:path"
|
import { join, resolve, win32 } from "node:path"
|
||||||
import {
|
import {
|
||||||
getOpenCodeConfigDir,
|
getOpenCodeConfigDir,
|
||||||
|
getOpenCodeConfigDirs,
|
||||||
getOpenCodeConfigPaths,
|
getOpenCodeConfigPaths,
|
||||||
isDevBuild,
|
isDevBuild,
|
||||||
detectExistingConfigDir,
|
detectExistingConfigDir,
|
||||||
@@ -45,7 +46,7 @@ describe("opencode-config-dir", () => {
|
|||||||
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
|
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
|
||||||
|
|
||||||
// then returns the custom path
|
// then returns the custom path
|
||||||
expect(result).toBe("/custom/opencode/path")
|
expect(result).toBe(resolve("/custom/opencode/path"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test("falls back to default when env var is not set", () => {
|
test("falls back to default when env var is not set", () => {
|
||||||
@@ -109,7 +110,23 @@ describe("opencode-config-dir", () => {
|
|||||||
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
|
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
|
||||||
|
|
||||||
// then OPENCODE_CONFIG_DIR takes priority
|
// then OPENCODE_CONFIG_DIR takes priority
|
||||||
expect(result).toBe("/custom/opencode/path")
|
expect(result).toBe(resolve("/custom/opencode/path"))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns both custom and default config directories for additive discovery", () => {
|
||||||
|
// 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 getOpenCodeConfigDirs is called
|
||||||
|
const result = getOpenCodeConfigDirs({ binary: "opencode", version: "1.0.200" })
|
||||||
|
|
||||||
|
// then the custom path stays first, but the default global path remains visible
|
||||||
|
expect(result).toEqual([
|
||||||
|
resolve("/custom/opencode/path"),
|
||||||
|
resolve("/xdg/config/opencode"),
|
||||||
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -163,7 +180,7 @@ describe("opencode-config-dir", () => {
|
|||||||
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
|
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200" })
|
||||||
|
|
||||||
// then returns $XDG_CONFIG_HOME/opencode
|
// then returns $XDG_CONFIG_HOME/opencode
|
||||||
expect(result).toBe("/custom/config/opencode")
|
expect(result).toBe(resolve("/custom/config/opencode"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns ~/.config/opencode on macOS", () => {
|
test("returns ~/.config/opencode on macOS", () => {
|
||||||
|
|||||||
@@ -55,16 +55,37 @@ function resolveConfigPath(pathValue: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCliConfigDir(): string {
|
function getCliDefaultConfigDir(): string {
|
||||||
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim()
|
|
||||||
if (envConfigDir) {
|
|
||||||
return resolveConfigPath(envConfigDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config")
|
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config")
|
||||||
return resolveConfigPath(join(xdgConfig, "opencode"))
|
return resolveConfigPath(join(xdgConfig, "opencode"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCliCustomConfigDir(): string | null {
|
||||||
|
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim()
|
||||||
|
if (!envConfigDir) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveConfigPath(envConfigDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCliConfigDir(): string {
|
||||||
|
return getCliCustomConfigDir() ?? getCliDefaultConfigDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOpenCodeConfigDirs(options: OpenCodeConfigDirOptions): string[] {
|
||||||
|
if (options.binary !== "opencode") {
|
||||||
|
return [getOpenCodeConfigDir(options)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(
|
||||||
|
new Set([
|
||||||
|
...(getCliCustomConfigDir() ? [getCliCustomConfigDir()!] : []),
|
||||||
|
getCliDefaultConfigDir(),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function getOpenCodeConfigDir(options: OpenCodeConfigDirOptions): string {
|
export function getOpenCodeConfigDir(options: OpenCodeConfigDirOptions): string {
|
||||||
const { binary, version, checkExisting = true } = options
|
const { binary, version, checkExisting = true } = options
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const ENV_KEYS = [
|
|||||||
"CLAUDE_PLUGINS_HOME",
|
"CLAUDE_PLUGINS_HOME",
|
||||||
"CLAUDE_SETTINGS_PATH",
|
"CLAUDE_SETTINGS_PATH",
|
||||||
"OPENCODE_CONFIG_DIR",
|
"OPENCODE_CONFIG_DIR",
|
||||||
|
"XDG_CONFIG_HOME",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
type EnvKey = (typeof ENV_KEYS)[number]
|
type EnvKey = (typeof ENV_KEYS)[number]
|
||||||
@@ -119,6 +120,7 @@ describe("slashcommand command discovery plugin integration", () => {
|
|||||||
CLAUDE_PLUGINS_HOME: process.env.CLAUDE_PLUGINS_HOME,
|
CLAUDE_PLUGINS_HOME: process.env.CLAUDE_PLUGINS_HOME,
|
||||||
CLAUDE_SETTINGS_PATH: process.env.CLAUDE_SETTINGS_PATH,
|
CLAUDE_SETTINGS_PATH: process.env.CLAUDE_SETTINGS_PATH,
|
||||||
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
|
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
|
||||||
|
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
||||||
}
|
}
|
||||||
const setup = writePluginFixture(tempDir)
|
const setup = writePluginFixture(tempDir)
|
||||||
projectDir = setup.projectDir
|
projectDir = setup.projectDir
|
||||||
@@ -193,6 +195,40 @@ Use parent opencode commit command.
|
|||||||
expect(commitCommand?.content).toContain("Use parent opencode commit command.")
|
expect(commitCommand?.content).toContain("Use parent opencode commit command.")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("discovers commands from both OPENCODE_CONFIG_DIR and the default global config directory", () => {
|
||||||
|
const defaultGlobalDir = join(tempDir, "xdg", "opencode", "commands")
|
||||||
|
const customGlobalDir = join(tempDir, "custom-opencode", "commands")
|
||||||
|
|
||||||
|
mkdirSync(defaultGlobalDir, { recursive: true })
|
||||||
|
mkdirSync(customGlobalDir, { recursive: true })
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(defaultGlobalDir, "global-default.md"),
|
||||||
|
`---
|
||||||
|
description: Default global opencode command
|
||||||
|
---
|
||||||
|
Use default global command.
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
writeFileSync(
|
||||||
|
join(customGlobalDir, "global-custom.md"),
|
||||||
|
`---
|
||||||
|
description: Custom global opencode command
|
||||||
|
---
|
||||||
|
Use custom global command.
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
|
||||||
|
process.env.XDG_CONFIG_HOME = join(tempDir, "xdg")
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = join(tempDir, "custom-opencode")
|
||||||
|
|
||||||
|
const commands = discoverCommandsSync(projectDir)
|
||||||
|
const names = commands.map(command => command.name)
|
||||||
|
|
||||||
|
expect(names).toContain("global-default")
|
||||||
|
expect(names).toContain("global-custom")
|
||||||
|
})
|
||||||
|
|
||||||
it("discovers ancestor project opencode commands from plural commands directory", () => {
|
it("discovers ancestor project opencode commands from plural commands directory", () => {
|
||||||
const projectRoot = join(projectDir, "workspace")
|
const projectRoot = join(projectDir, "workspace")
|
||||||
const childDir = join(projectRoot, "apps", "cli")
|
const childDir = join(projectRoot, "apps", "cli")
|
||||||
|
|||||||
Reference in New Issue
Block a user