fix(skill-loader): deterministic collision handling for skill names
- Separate directory and file entries, process directories first
- Use Map to deduplicate skills by name (first-wins)
- Directory skills (SKILL.md, {dir}.md) take precedence over file skills (*.md)
- Add test for collision scenario
Addresses Oracle P2 review feedback from PR #1254
This commit is contained in:
@@ -140,53 +140,59 @@ async function loadSkillsFromDir(
|
||||
maxDepth: number = 2
|
||||
): Promise<LoadedSkill[]> {
|
||||
const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => [])
|
||||
const skills: LoadedSkill[] = []
|
||||
const skillMap = new Map<string, LoadedSkill>()
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith(".")) continue
|
||||
const directories = entries.filter(e => !e.name.startsWith(".") && (e.isDirectory() || e.isSymbolicLink()))
|
||||
const files = entries.filter(e => !e.name.startsWith(".") && !e.isDirectory() && !e.isSymbolicLink() && isMarkdownFile(e))
|
||||
|
||||
for (const entry of directories) {
|
||||
const entryPath = join(skillsDir, entry.name)
|
||||
const resolvedPath = await resolveSymlinkAsync(entryPath)
|
||||
const dirName = entry.name
|
||||
|
||||
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
||||
const resolvedPath = await resolveSymlinkAsync(entryPath)
|
||||
const dirName = entry.name
|
||||
|
||||
const skillMdPath = join(resolvedPath, "SKILL.md")
|
||||
try {
|
||||
await fs.access(skillMdPath)
|
||||
const skill = await loadSkillFromPath(skillMdPath, resolvedPath, dirName, scope, namePrefix)
|
||||
if (skill) skills.push(skill)
|
||||
continue
|
||||
} catch {
|
||||
const skillMdPath = join(resolvedPath, "SKILL.md")
|
||||
try {
|
||||
await fs.access(skillMdPath)
|
||||
const skill = await loadSkillFromPath(skillMdPath, resolvedPath, dirName, scope, namePrefix)
|
||||
if (skill && !skillMap.has(skill.name)) {
|
||||
skillMap.set(skill.name, skill)
|
||||
}
|
||||
|
||||
const namedSkillMdPath = join(resolvedPath, `${dirName}.md`)
|
||||
try {
|
||||
await fs.access(namedSkillMdPath)
|
||||
const skill = await loadSkillFromPath(namedSkillMdPath, resolvedPath, dirName, scope, namePrefix)
|
||||
if (skill) skills.push(skill)
|
||||
continue
|
||||
} catch {
|
||||
}
|
||||
|
||||
// Recurse into subdirectories if no skill found and within depth limit
|
||||
if (depth < maxDepth) {
|
||||
const newPrefix = namePrefix ? `${namePrefix}/${dirName}` : dirName
|
||||
const nestedSkills = await loadSkillsFromDir(resolvedPath, scope, newPrefix, depth + 1, maxDepth)
|
||||
skills.push(...nestedSkills)
|
||||
}
|
||||
|
||||
continue
|
||||
} catch {
|
||||
}
|
||||
|
||||
if (isMarkdownFile(entry)) {
|
||||
const baseName = basename(entry.name, ".md")
|
||||
const skill = await loadSkillFromPath(entryPath, skillsDir, baseName, scope, namePrefix)
|
||||
if (skill) skills.push(skill)
|
||||
const namedSkillMdPath = join(resolvedPath, `${dirName}.md`)
|
||||
try {
|
||||
await fs.access(namedSkillMdPath)
|
||||
const skill = await loadSkillFromPath(namedSkillMdPath, resolvedPath, dirName, scope, namePrefix)
|
||||
if (skill && !skillMap.has(skill.name)) {
|
||||
skillMap.set(skill.name, skill)
|
||||
}
|
||||
continue
|
||||
} catch {
|
||||
}
|
||||
|
||||
if (depth < maxDepth) {
|
||||
const newPrefix = namePrefix ? `${namePrefix}/${dirName}` : dirName
|
||||
const nestedSkills = await loadSkillsFromDir(resolvedPath, scope, newPrefix, depth + 1, maxDepth)
|
||||
for (const nestedSkill of nestedSkills) {
|
||||
if (!skillMap.has(nestedSkill.name)) {
|
||||
skillMap.set(nestedSkill.name, nestedSkill)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return skills
|
||||
for (const entry of files) {
|
||||
const entryPath = join(skillsDir, entry.name)
|
||||
const baseName = basename(entry.name, ".md")
|
||||
const skill = await loadSkillFromPath(entryPath, skillsDir, baseName, scope, namePrefix)
|
||||
if (skill && !skillMap.has(skill.name)) {
|
||||
skillMap.set(skill.name, skill)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(skillMap.values())
|
||||
}
|
||||
|
||||
function skillsToRecord(skills: LoadedSkill[]): Record<string, CommandDefinition> {
|
||||
|
||||
Reference in New Issue
Block a user