diff --git a/test/test-installation-components.js b/test/test-installation-components.js index fd3a854ba..0b977884f 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -1722,6 +1722,21 @@ async function runTests() { const rescannedModules29 = await generator29.scanInstalledModules(tempFixture29); assert(rescannedModules29.includes('agent-only-mod'), 'scanInstalledModules recognizes native-agent-only module'); + // Test scanInstalledModules recognizes multi-entry manifests keyed under SKILL.md + const multiEntryModDir29 = path.join(tempFixture29, 'multi-entry-mod'); + await fs.ensureDir(path.join(multiEntryModDir29, 'deep', 'nested', 'bmad-tea')); + await fs.writeFile( + path.join(multiEntryModDir29, 'deep', 'nested', 'bmad-tea', 'bmad-skill-manifest.yaml'), + 'SKILL.md:\n type: agent\n canonicalId: bmad-tea\n', + ); + await fs.writeFile( + path.join(multiEntryModDir29, 'deep', 'nested', 'bmad-tea', 'SKILL.md'), + '---\nname: bmad-tea\ndescription: desc\n---\n\nAgent menu.\n', + ); + + const rescannedModules29b = await generator29.scanInstalledModules(tempFixture29); + assert(rescannedModules29b.includes('multi-entry-mod'), 'scanInstalledModules recognizes multi-entry native-agent module'); + // skill-manifest.csv should include the native agent entrypoint const skillManifestCsv29 = await fs.readFile(path.join(tempFixture29, '_config', 'skill-manifest.csv'), 'utf8'); assert(skillManifestCsv29.includes('bmad-tea'), 'skill-manifest.csv includes native type:agent SKILL.md entrypoint'); diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 7932f1516..c9b85db27 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -60,6 +60,19 @@ class ManifestGenerator { return artifactType === 'skill' || artifactType === 'agent'; } + /** + * Check whether a loaded bmad-skill-manifest.yaml declares a native + * SKILL.md entrypoint, either as a single-entry manifest or a multi-entry map. + * @param {Object|null} manifest - Loaded manifest + * @returns {boolean} True when the manifest contains a native skill/agent entrypoint + */ + hasNativeSkillManifest(manifest) { + if (!manifest) return false; + if (manifest.__single) return this.isNativeSkillDirType(manifest.__single.type); + + return Object.values(manifest).some((entry) => this.isNativeSkillDirType(entry?.type)); + } + /** * Clean text for CSV output by normalizing whitespace. * Note: Quote escaping is handled by escapeCsv() at write time. @@ -1391,8 +1404,8 @@ class ManifestGenerator { } /** - * Recursively check if a directory tree contains a bmad-skill-manifest.yaml with - * type: skill or type: agent. + * Recursively check if a directory tree contains a bmad-skill-manifest.yaml that + * declares a native SKILL.md entrypoint (type: skill or type: agent). * Skips directories starting with . or _. * @param {string} dir - Directory to search * @returns {boolean} True if a skill manifest is found @@ -1407,10 +1420,7 @@ class ManifestGenerator { // Check for manifest in this directory const manifest = await this.loadSkillManifest(dir); - if (manifest) { - const type = this.getArtifactType(manifest, 'workflow.md'); - if (this.isNativeSkillDirType(type)) return true; - } + if (this.hasNativeSkillManifest(manifest)) return true; // Recurse into subdirectories for (const entry of entries) {