perf(skill-loader): parallelize directory scanning
- 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)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||||
|
import { promises as fs } from "fs"
|
||||||
import { join, basename } from "path"
|
import { join, basename } from "path"
|
||||||
import { homedir } from "os"
|
import { homedir } from "os"
|
||||||
import yaml from "js-yaml"
|
import yaml from "js-yaml"
|
||||||
@@ -114,6 +115,62 @@ $ARGUMENTS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSkillFromPathAsync(
|
||||||
|
skillPath: string,
|
||||||
|
resolvedPath: string,
|
||||||
|
defaultName: string,
|
||||||
|
scope: SkillScope
|
||||||
|
): Promise<LoadedSkill | null> {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(skillPath, "utf-8")
|
||||||
|
const { data, body } = parseFrontmatter<SkillMetadata>(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 = `<skill-instruction>
|
||||||
|
Base directory for this skill: ${resolvedPath}/
|
||||||
|
File references (@path) in this skill are relative to this directory.
|
||||||
|
|
||||||
|
${body.trim()}
|
||||||
|
</skill-instruction>
|
||||||
|
|
||||||
|
<user-request>
|
||||||
|
$ARGUMENTS
|
||||||
|
</user-request>`
|
||||||
|
|
||||||
|
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:
|
* Load skills from a directory, supporting BOTH patterns:
|
||||||
* - Directory with SKILL.md: skill-name/SKILL.md
|
* - Directory with SKILL.md: skill-name/SKILL.md
|
||||||
@@ -164,6 +221,53 @@ function loadSkillsFromDir(skillsDir: string, scope: SkillScope): LoadedSkill[]
|
|||||||
return skills
|
return skills
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async version of loadSkillsFromDir using Promise-based fs operations.
|
||||||
|
*/
|
||||||
|
async function loadSkillsFromDirAsync(skillsDir: string, scope: SkillScope): Promise<LoadedSkill[]> {
|
||||||
|
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<string, CommandDefinition> {
|
function skillsToRecord(skills: LoadedSkill[]): Record<string, CommandDefinition> {
|
||||||
const result: Record<string, CommandDefinition> = {}
|
const result: Record<string, CommandDefinition> = {}
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
@@ -286,3 +390,41 @@ export function discoverOpencodeProjectSkills(): LoadedSkill[] {
|
|||||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
||||||
return loadSkillsFromDir(opencodeProjectDir, "opencode-project")
|
return loadSkillsFromDir(opencodeProjectDir, "opencode-project")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function discoverUserClaudeSkillsAsync(): Promise<LoadedSkill[]> {
|
||||||
|
const userSkillsDir = join(getClaudeConfigDir(), "skills")
|
||||||
|
return loadSkillsFromDirAsync(userSkillsDir, "user")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discoverProjectClaudeSkillsAsync(): Promise<LoadedSkill[]> {
|
||||||
|
const projectSkillsDir = join(process.cwd(), ".claude", "skills")
|
||||||
|
return loadSkillsFromDirAsync(projectSkillsDir, "project")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discoverOpencodeGlobalSkillsAsync(): Promise<LoadedSkill[]> {
|
||||||
|
const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill")
|
||||||
|
return loadSkillsFromDirAsync(opencodeSkillsDir, "opencode")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discoverOpencodeProjectSkillsAsync(): Promise<LoadedSkill[]> {
|
||||||
|
const opencodeProjectDir = join(process.cwd(), ".opencode", "skill")
|
||||||
|
return loadSkillsFromDirAsync(opencodeProjectDir, "opencode-project")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discoverAllSkillsAsync(options: DiscoverSkillsOptions = {}): Promise<LoadedSkill[]> {
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user