From 55b80fb7cd24a38a6076c74cb98939190c9d2b76 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 12 Mar 2026 19:53:30 +0900 Subject: [PATCH] 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. --- src/features/opencode-skill-loader/loader.ts | 18 +++--- src/shared/opencode-command-dirs.test.ts | 66 ++++++++++++++++++++ src/shared/opencode-command-dirs.ts | 12 ++++ 3 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 src/shared/opencode-command-dirs.test.ts diff --git a/src/features/opencode-skill-loader/loader.ts b/src/features/opencode-skill-loader/loader.ts index 0a5a98aad..205267e3e 100644 --- a/src/features/opencode-skill-loader/loader.ts +++ b/src/features/opencode-skill-loader/loader.ts @@ -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> { - 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> { @@ -107,9 +109,11 @@ export async function discoverProjectClaudeSkills(directory?: string): Promise { - 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 { diff --git a/src/shared/opencode-command-dirs.test.ts b/src/shared/opencode-command-dirs.test.ts new file mode 100644 index 000000000..02ffb871f --- /dev/null +++ b/src/shared/opencode-command-dirs.test.ts @@ -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) + }) + }) + }) + }) +}) diff --git a/src/shared/opencode-command-dirs.ts b/src/shared/opencode-command-dirs.ts index 88b5578ba..456085730 100644 --- a/src/shared/opencode-command-dirs.ts +++ b/src/shared/opencode-command-dirs.ts @@ -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")] : []), + ]) + ) +}