fix(skill-loader): discover skills from parent config dir when using profiles

OPENCODE_CONFIG_DIR pointing to profiles/ subdirectory caused skills at
~/.config/opencode/skills/ to be invisible. Added getOpenCodeSkillDirs()
with the same parent-dir fallback that getOpenCodeCommandDirs() uses.
This commit is contained in:
YeonGyu-Kim
2026-03-12 19:53:30 +09:00
parent c85b6adb7d
commit 55b80fb7cd
3 changed files with 89 additions and 7 deletions

View File

@@ -2,6 +2,7 @@ import { join } from "path"
import { homedir } from "os"
import { getClaudeConfigDir } from "../../shared/claude-config-dir"
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
import { getOpenCodeSkillDirs } from "../../shared/opencode-command-dirs"
import type { CommandDefinition } from "../claude-code-command-loader/types"
import type { LoadedSkill } from "./types"
import { skillsToCommandDefinitionRecord } from "./skill-definition-record"
@@ -21,10 +22,11 @@ export async function loadProjectSkills(directory?: string): Promise<Record<stri
}
export async function loadOpencodeGlobalSkills(): Promise<Record<string, CommandDefinition>> {
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
const opencodeSkillsDir = join(configDir, "skills")
const skills = await loadSkillsFromDir({ skillsDir: opencodeSkillsDir, scope: "opencode" })
return skillsToCommandDefinitionRecord(skills)
const skillDirs = getOpenCodeSkillDirs({ binary: "opencode" })
const allSkills = await Promise.all(
skillDirs.map(skillsDir => loadSkillsFromDir({ skillsDir, scope: "opencode" }))
)
return skillsToCommandDefinitionRecord(deduplicateSkillsByName(allSkills.flat()))
}
export async function loadOpencodeProjectSkills(directory?: string): Promise<Record<string, CommandDefinition>> {
@@ -107,9 +109,11 @@ export async function discoverProjectClaudeSkills(directory?: string): Promise<L
}
export async function discoverOpencodeGlobalSkills(): Promise<LoadedSkill[]> {
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
const opencodeSkillsDir = join(configDir, "skills")
return loadSkillsFromDir({ skillsDir: opencodeSkillsDir, scope: "opencode" })
const skillDirs = getOpenCodeSkillDirs({ binary: "opencode" })
const allSkills = await Promise.all(
skillDirs.map(skillsDir => loadSkillsFromDir({ skillsDir, scope: "opencode" }))
)
return deduplicateSkillsByName(allSkills.flat())
}
export async function discoverOpencodeProjectSkills(directory?: string): Promise<LoadedSkill[]> {

View File

@@ -0,0 +1,66 @@
import { describe, expect, it, mock, beforeEach, afterEach } from "bun:test"
import { join } from "node:path"
describe("opencode-command-dirs", () => {
let originalEnv: string | undefined
beforeEach(() => {
originalEnv = process.env.OPENCODE_CONFIG_DIR
})
afterEach(() => {
if (originalEnv !== undefined) {
process.env.OPENCODE_CONFIG_DIR = originalEnv
} else {
delete process.env.OPENCODE_CONFIG_DIR
}
})
describe("getOpenCodeSkillDirs", () => {
describe("#given config dir inside profiles/", () => {
describe("#when getOpenCodeSkillDirs is called", () => {
it("#then returns both profile and parent skill dirs", async () => {
process.env.OPENCODE_CONFIG_DIR = "/home/user/.config/opencode/profiles/opus"
const { getOpenCodeSkillDirs } = await import("./opencode-command-dirs")
const dirs = getOpenCodeSkillDirs({ binary: "opencode" })
expect(dirs).toContain("/home/user/.config/opencode/profiles/opus/skills")
expect(dirs).toContain("/home/user/.config/opencode/skills")
expect(dirs).toHaveLength(2)
})
})
})
describe("#given config dir NOT inside profiles/", () => {
describe("#when getOpenCodeSkillDirs is called", () => {
it("#then returns only the config dir skills", async () => {
process.env.OPENCODE_CONFIG_DIR = "/home/user/.config/opencode"
const { getOpenCodeSkillDirs } = await import("./opencode-command-dirs")
const dirs = getOpenCodeSkillDirs({ binary: "opencode" })
expect(dirs).toContain("/home/user/.config/opencode/skills")
expect(dirs).toHaveLength(1)
})
})
})
})
describe("getOpenCodeCommandDirs", () => {
describe("#given config dir inside profiles/", () => {
describe("#when getOpenCodeCommandDirs is called", () => {
it("#then returns both profile and parent command dirs", async () => {
process.env.OPENCODE_CONFIG_DIR = "/home/user/.config/opencode/profiles/opus"
const { getOpenCodeCommandDirs } = await import("./opencode-command-dirs")
const dirs = getOpenCodeCommandDirs({ binary: "opencode" })
expect(dirs).toContain("/home/user/.config/opencode/profiles/opus/command")
expect(dirs).toContain("/home/user/.config/opencode/command")
expect(dirs).toHaveLength(2)
})
})
})
})
})

View File

@@ -22,3 +22,15 @@ export function getOpenCodeCommandDirs(options: OpenCodeConfigDirOptions): strin
])
)
}
export function getOpenCodeSkillDirs(options: OpenCodeConfigDirOptions): string[] {
const configDir = getOpenCodeConfigDir(options)
const parentConfigDir = getParentOpencodeConfigDir(configDir)
return Array.from(
new Set([
join(configDir, "skills"),
...(parentConfigDir ? [join(parentConfigDir, "skills")] : []),
])
)
}