From 6575dfcbc480f17c8f8d7d394c37441c4a8b0d48 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 4 Jan 2026 21:25:53 +0900 Subject: [PATCH] perf(skill-loader): parallelize directory scanning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add async versions of skill discovery functions - Create discoverAllSkillsAsync() with Promise.all parallelization - Use fs.promises for async file operations - Keep sync versions for backward compatibility 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/features/opencode-skill-loader/loader.ts | 142 +++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/src/features/opencode-skill-loader/loader.ts b/src/features/opencode-skill-loader/loader.ts index b0bbd9b..9934b5c 100644 --- a/src/features/opencode-skill-loader/loader.ts +++ b/src/features/opencode-skill-loader/loader.ts @@ -1,4 +1,5 @@ import { existsSync, readdirSync, readFileSync } from "fs" +import { promises as fs } from "fs" import { join, basename } from "path" import { homedir } from "os" import yaml from "js-yaml" @@ -114,6 +115,62 @@ $ARGUMENTS } } +async function loadSkillFromPathAsync( + skillPath: string, + resolvedPath: string, + defaultName: string, + scope: SkillScope +): Promise { + try { + const content = await fs.readFile(skillPath, "utf-8") + const { data, body } = parseFrontmatter(content) + const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content) + const mcpJsonMcp = loadMcpJsonFromDir(resolvedPath) + const mcpConfig = mcpJsonMcp || frontmatterMcp + + const skillName = data.name || defaultName + const originalDescription = data.description || "" + const isOpencodeSource = scope === "opencode" || scope === "opencode-project" + const formattedDescription = `(${scope} - Skill) ${originalDescription}` + + const wrappedTemplate = ` +Base directory for this skill: ${resolvedPath}/ +File references (@path) in this skill are relative to this directory. + +${body.trim()} + + + +$ARGUMENTS +` + + const definition: CommandDefinition = { + name: skillName, + description: formattedDescription, + template: wrappedTemplate, + model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"), + agent: data.agent, + subtask: data.subtask, + argumentHint: data["argument-hint"], + } + + return { + name: skillName, + path: skillPath, + resolvedPath, + definition, + scope, + license: data.license, + compatibility: data.compatibility, + metadata: data.metadata, + allowedTools: parseAllowedTools(data["allowed-tools"]), + mcpConfig, + } + } catch { + return null + } +} + /** * Load skills from a directory, supporting BOTH patterns: * - Directory with SKILL.md: skill-name/SKILL.md @@ -164,6 +221,53 @@ function loadSkillsFromDir(skillsDir: string, scope: SkillScope): LoadedSkill[] return skills } +/** + * Async version of loadSkillsFromDir using Promise-based fs operations. + */ +async function loadSkillsFromDirAsync(skillsDir: string, scope: SkillScope): Promise { + const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => []) + const skills: LoadedSkill[] = [] + + for (const entry of entries) { + if (entry.name.startsWith(".")) continue + + const entryPath = join(skillsDir, entry.name) + + if (entry.isDirectory() || entry.isSymbolicLink()) { + const resolvedPath = resolveSymlink(entryPath) + const dirName = entry.name + + const skillMdPath = join(resolvedPath, "SKILL.md") + try { + await fs.access(skillMdPath) + const skill = await loadSkillFromPathAsync(skillMdPath, resolvedPath, dirName, scope) + if (skill) skills.push(skill) + continue + } catch { + } + + const namedSkillMdPath = join(resolvedPath, `${dirName}.md`) + try { + await fs.access(namedSkillMdPath) + const skill = await loadSkillFromPathAsync(namedSkillMdPath, resolvedPath, dirName, scope) + if (skill) skills.push(skill) + continue + } catch { + } + + continue + } + + if (isMarkdownFile(entry)) { + const skillName = basename(entry.name, ".md") + const skill = await loadSkillFromPathAsync(entryPath, skillsDir, skillName, scope) + if (skill) skills.push(skill) + } + } + + return skills +} + function skillsToRecord(skills: LoadedSkill[]): Record { const result: Record = {} for (const skill of skills) { @@ -286,3 +390,41 @@ export function discoverOpencodeProjectSkills(): LoadedSkill[] { const opencodeProjectDir = join(process.cwd(), ".opencode", "skill") return loadSkillsFromDir(opencodeProjectDir, "opencode-project") } + +export async function discoverUserClaudeSkillsAsync(): Promise { + const userSkillsDir = join(getClaudeConfigDir(), "skills") + return loadSkillsFromDirAsync(userSkillsDir, "user") +} + +export async function discoverProjectClaudeSkillsAsync(): Promise { + const projectSkillsDir = join(process.cwd(), ".claude", "skills") + return loadSkillsFromDirAsync(projectSkillsDir, "project") +} + +export async function discoverOpencodeGlobalSkillsAsync(): Promise { + const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill") + return loadSkillsFromDirAsync(opencodeSkillsDir, "opencode") +} + +export async function discoverOpencodeProjectSkillsAsync(): Promise { + const opencodeProjectDir = join(process.cwd(), ".opencode", "skill") + return loadSkillsFromDirAsync(opencodeProjectDir, "opencode-project") +} + +export async function discoverAllSkillsAsync(options: DiscoverSkillsOptions = {}): Promise { + const { includeClaudeCodePaths = true } = options + + const opencodeProjectSkills = await discoverOpencodeProjectSkillsAsync() + const opencodeGlobalSkills = await discoverOpencodeGlobalSkillsAsync() + + if (!includeClaudeCodePaths) { + return [...opencodeProjectSkills, ...opencodeGlobalSkills] + } + + const [projectSkills, userSkills] = await Promise.all([ + discoverProjectClaudeSkillsAsync(), + discoverUserClaudeSkillsAsync(), + ]) + + return [...opencodeProjectSkills, ...projectSkills, ...opencodeGlobalSkills, ...userSkills] +}