Merge pull request #2936 from HOYALIM/fix-2696-skill-xml-names
fix: preserve nested async skill names in discovery
This commit is contained in:
@@ -107,6 +107,78 @@ Direct skill.
|
||||
expect(skills[0].name).toBe("direct-skill")
|
||||
})
|
||||
|
||||
it("preserves nested skill path names during recursive discovery", async () => {
|
||||
// given
|
||||
const nestedSkillDir = join(SKILLS_DIR, "superpowers", "brainstorming")
|
||||
mkdirSync(nestedSkillDir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(nestedSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: brainstorming
|
||||
description: Nested brainstorming skill
|
||||
---
|
||||
Nested skill.
|
||||
`
|
||||
)
|
||||
|
||||
// when
|
||||
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
||||
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
||||
|
||||
// then
|
||||
expect(skills).toHaveLength(1)
|
||||
expect(skills[0]?.name).toBe("superpowers/brainstorming")
|
||||
expect(skills[0]?.definition.name).toBe("superpowers/brainstorming")
|
||||
})
|
||||
|
||||
it("preserves nested skill path names for nested {dirName}.md discovery", async () => {
|
||||
// given
|
||||
const nestedSkillDir = join(SKILLS_DIR, "superpowers", "brainstorming")
|
||||
mkdirSync(nestedSkillDir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(nestedSkillDir, "brainstorming.md"),
|
||||
`---
|
||||
name: brainstorming
|
||||
description: Nested brainstorming skill
|
||||
---
|
||||
Nested skill.
|
||||
`
|
||||
)
|
||||
|
||||
// when
|
||||
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
||||
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
||||
|
||||
// then
|
||||
expect(skills).toHaveLength(1)
|
||||
expect(skills[0]?.name).toBe("superpowers/brainstorming")
|
||||
expect(skills[0]?.definition.name).toBe("superpowers/brainstorming")
|
||||
})
|
||||
|
||||
it("preserves nested skill path names for nested direct markdown discovery", async () => {
|
||||
// given
|
||||
const nestedSkillDir = join(SKILLS_DIR, "superpowers")
|
||||
mkdirSync(nestedSkillDir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(nestedSkillDir, "brainstorming.md"),
|
||||
`---
|
||||
name: brainstorming
|
||||
description: Nested brainstorming skill
|
||||
---
|
||||
Nested skill.
|
||||
`
|
||||
)
|
||||
|
||||
// when
|
||||
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
||||
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
||||
|
||||
// then
|
||||
expect(skills).toHaveLength(1)
|
||||
expect(skills[0]?.name).toBe("superpowers/brainstorming")
|
||||
expect(skills[0]?.definition.name).toBe("superpowers/brainstorming")
|
||||
})
|
||||
|
||||
it("skips entries starting with dot", async () => {
|
||||
// given
|
||||
const validContent = `---
|
||||
|
||||
@@ -75,7 +75,8 @@ export async function loadSkillFromPathAsync(
|
||||
skillPath: string,
|
||||
resolvedPath: string,
|
||||
defaultName: string,
|
||||
scope: SkillScope
|
||||
scope: SkillScope,
|
||||
namePrefix = ""
|
||||
): Promise<LoadedSkill | null> {
|
||||
try {
|
||||
const content = await readFile(skillPath, "utf-8")
|
||||
@@ -86,7 +87,8 @@ export async function loadSkillFromPathAsync(
|
||||
const mcpJsonMcp = await loadMcpJsonFromDirAsync(resolvedPath)
|
||||
const mcpConfig = mcpJsonMcp || frontmatterMcp
|
||||
|
||||
const skillName = data.name || defaultName
|
||||
const baseName = data.name || defaultName
|
||||
const skillName = namePrefix ? `${namePrefix}/${baseName}` : baseName
|
||||
const originalDescription = data.description || ""
|
||||
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
||||
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
|
||||
@@ -142,11 +144,17 @@ function parseAllowedTools(allowedTools: string | string[] | undefined): string[
|
||||
return allowedTools.split(/\s+/).filter(Boolean)
|
||||
}
|
||||
|
||||
export async function discoverSkillsInDirAsync(skillsDir: string): Promise<LoadedSkill[]> {
|
||||
export async function discoverSkillsInDirAsync(
|
||||
skillsDir: string,
|
||||
scope: SkillScope = "opencode-project",
|
||||
namePrefix = "",
|
||||
depth = 0,
|
||||
maxDepth = 2
|
||||
): Promise<LoadedSkill[]> {
|
||||
try {
|
||||
const entries = await readdir(skillsDir, { withFileTypes: true })
|
||||
|
||||
const processEntry = async (entry: Dirent): Promise<LoadedSkill | null> => {
|
||||
const processEntry = async (entry: Dirent): Promise<LoadedSkill | LoadedSkill[] | null> => {
|
||||
if (entry.name.startsWith(".")) return null
|
||||
|
||||
const entryPath = join(skillsDir, entry.name)
|
||||
@@ -158,28 +166,44 @@ export async function discoverSkillsInDirAsync(skillsDir: string): Promise<Loade
|
||||
const skillMdPath = join(resolvedPath, "SKILL.md")
|
||||
try {
|
||||
await readFile(skillMdPath, "utf-8")
|
||||
return await loadSkillFromPathAsync(skillMdPath, resolvedPath, dirName, "opencode-project")
|
||||
return await loadSkillFromPathAsync(skillMdPath, resolvedPath, dirName, scope, namePrefix)
|
||||
} catch {
|
||||
const namedSkillMdPath = join(resolvedPath, `${dirName}.md`)
|
||||
try {
|
||||
await readFile(namedSkillMdPath, "utf-8")
|
||||
return await loadSkillFromPathAsync(namedSkillMdPath, resolvedPath, dirName, "opencode-project")
|
||||
return await loadSkillFromPathAsync(namedSkillMdPath, resolvedPath, dirName, scope, namePrefix)
|
||||
} catch {
|
||||
return null
|
||||
if (depth >= maxDepth) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestedPrefix = namePrefix ? `${namePrefix}/${dirName}` : dirName
|
||||
const nestedSkills = await discoverSkillsInDirAsync(
|
||||
resolvedPath,
|
||||
scope,
|
||||
nestedPrefix,
|
||||
depth + 1,
|
||||
maxDepth
|
||||
)
|
||||
|
||||
return nestedSkills.length > 0 ? nestedSkills : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isMarkdownFile(entry)) {
|
||||
const skillName = basename(entry.name, ".md")
|
||||
return await loadSkillFromPathAsync(entryPath, skillsDir, skillName, "opencode-project")
|
||||
return await loadSkillFromPathAsync(entryPath, skillsDir, skillName, scope, namePrefix)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const skillPromises = await mapWithConcurrency(entries, processEntry, 16)
|
||||
return skillPromises.filter((skill): skill is LoadedSkill => skill !== null)
|
||||
return skillPromises.flatMap((skill): LoadedSkill[] => {
|
||||
if (skill === null) return []
|
||||
return Array.isArray(skill) ? skill : [skill]
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user