* Recursively collect .md files under a directory. Uses withFileTypes to * avoid a stat per entry. Returns absolute paths so error messages stay * readable.
( dir: string, isSkillsDir: boolean, )
| 716 | * readable. |
| 717 | */ |
| 718 | async function collectMarkdown( |
| 719 | dir: string, |
| 720 | isSkillsDir: boolean, |
| 721 | ): Promise<string[]> { |
| 722 | let entries: Dirent[] |
| 723 | try { |
| 724 | entries = await readdir(dir, { withFileTypes: true }) |
| 725 | } catch (e: unknown) { |
| 726 | const code = getErrnoCode(e) |
| 727 | if (code === 'ENOENT' || code === 'ENOTDIR') return [] |
| 728 | throw e |
| 729 | } |
| 730 | |
| 731 | // Skills use <name>/SKILL.md — only descend one level, only collect SKILL.md. |
| 732 | // Matches the runtime loader: single .md files in skills/ are NOT loaded, |
| 733 | // and subdirectories of a skill dir aren't scanned. Paths are speculative |
| 734 | // (the subdir may lack SKILL.md); the caller handles ENOENT. |
| 735 | if (isSkillsDir) { |
| 736 | return entries |
| 737 | .filter(e => e.isDirectory()) |
| 738 | .map(e => path.join(dir, e.name, 'SKILL.md')) |
| 739 | } |
| 740 | |
| 741 | // Commands/agents: recurse and collect all .md files. |
| 742 | const out: string[] = [] |
| 743 | for (const entry of entries) { |
| 744 | const full = path.join(dir, entry.name) |
| 745 | if (entry.isDirectory()) { |
| 746 | out.push(...(await collectMarkdown(full, false))) |
| 747 | } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) { |
| 748 | out.push(full) |
| 749 | } |
| 750 | } |
| 751 | return out |
| 752 | } |
| 753 | |
| 754 | /** |
| 755 | * Validate the content files inside a plugin directory — skills, agents, |
no test coverage detected