diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 1654bc110..d10818efd 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -81,6 +81,60 @@ async function createTestBmadFixture() { return fixtureDir; } +async function createSkillCollisionFixture() { + const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-skill-collision-')); + const fixtureDir = path.join(fixtureRoot, '_bmad'); + const configDir = path.join(fixtureDir, '_config'); + await fs.ensureDir(configDir); + + await fs.writeFile( + path.join(configDir, 'agent-manifest.csv'), + [ + 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path,canonicalId', + '"bmad-master","BMAD Master","","","","","","","","core","_bmad/core/agents/bmad-master.md","bmad-master"', + '', + ].join('\n'), + ); + + await fs.writeFile( + path.join(configDir, 'workflow-manifest.csv'), + [ + 'name,description,module,path,canonicalId', + '"help","Workflow help","core","_bmad/core/workflows/help/workflow.md","bmad-help"', + '', + ].join('\n'), + ); + + await fs.writeFile(path.join(configDir, 'task-manifest.csv'), 'name,displayName,description,module,path,standalone,canonicalId\n'); + await fs.writeFile(path.join(configDir, 'tool-manifest.csv'), 'name,displayName,description,module,path,standalone,canonicalId\n'); + await fs.writeFile( + path.join(configDir, 'skill-manifest.csv'), + [ + 'canonicalId,name,description,module,path,install_to_bmad', + '"bmad-help","bmad-help","Native help skill","core","_bmad/core/tasks/bmad-help/SKILL.md","true"', + '', + ].join('\n'), + ); + + const skillDir = path.join(fixtureDir, 'core', 'tasks', 'bmad-help'); + await fs.ensureDir(skillDir); + await fs.writeFile( + path.join(skillDir, 'SKILL.md'), + ['---', 'name: bmad-help', 'description: Native help skill', '---', '', 'Use this skill directly.'].join('\n'), + ); + + const agentDir = path.join(fixtureDir, 'core', 'agents'); + await fs.ensureDir(agentDir); + await fs.writeFile( + path.join(agentDir, 'bmad-master.md'), + ['---', 'name: BMAD Master', 'description: Master agent', '---', '', '', ''].join( + '\n', + ), + ); + + return { root: fixtureRoot, bmadDir: fixtureDir }; +} + /** * Test Suite */ @@ -1770,6 +1824,50 @@ async function runTests() { console.log(''); + // ============================================================ + // Test 31: Skill-format installs report unique skill directories + // ============================================================ + console.log(`${colors.yellow}Test Suite 31: Skill Count Reporting${colors.reset}\n`); + + let collisionFixtureRoot = null; + let collisionProjectDir = null; + + try { + clearCache(); + const collisionFixture = await createSkillCollisionFixture(); + collisionFixtureRoot = collisionFixture.root; + collisionProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-antigravity-test-')); + + const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); + const result = await ideManager.setup('antigravity', collisionProjectDir, collisionFixture.bmadDir, { + silent: true, + selectedModules: ['core'], + }); + + assert(result.success === true, 'Antigravity setup succeeds with overlapping skill names'); + assert(result.detail === '2 skills, 2 agents', 'Installer detail reports total skills and total agents'); + assert(result.handlerResult.results.skillDirectories === 2, 'Result exposes unique skill directory count'); + assert(result.handlerResult.results.agents === 2, 'Result retains generated agent write count'); + assert(result.handlerResult.results.workflows === 1, 'Result retains generated workflow count'); + assert(result.handlerResult.results.skills === 1, 'Result retains verbatim skill count'); + assert( + await fs.pathExists(path.join(collisionProjectDir, '.agent', 'skills', 'bmad-agent-bmad-master', 'SKILL.md')), + 'Agent skill directory is created', + ); + assert( + await fs.pathExists(path.join(collisionProjectDir, '.agent', 'skills', 'bmad-help', 'SKILL.md')), + 'Overlapping skill directory is created once', + ); + } catch (error) { + assert(false, 'Skill-format unique count test succeeds', error.message); + } finally { + if (collisionProjectDir) await fs.remove(collisionProjectDir).catch(() => {}); + if (collisionFixtureRoot) await fs.remove(collisionFixtureRoot).catch(() => {}); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index c9ea83182..85864145f 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -1153,12 +1153,6 @@ class Installer { preservedModules: modulesForCsvPreserve, }); - addResult( - 'Manifests', - 'ok', - `${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`, - ); - // Merge help catalogs message('Generating help catalog...'); await this.mergeModuleHelpCatalogs(bmadDir); @@ -1379,10 +1373,27 @@ class Installer { */ async renderInstallSummary(results, context = {}) { const color = await prompts.getColor(); + const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase())); // Build step lines with status indicators const lines = []; for (const r of results) { + let stepLabel = null; + + if (r.status !== 'ok') { + stepLabel = r.step; + } else if (r.step === 'Core') { + stepLabel = 'BMAD'; + } else if (r.step.startsWith('Module: ')) { + stepLabel = r.step; + } else if (selectedIdes.has(String(r.step).toLowerCase())) { + stepLabel = r.step; + } + + if (!stepLabel) { + continue; + } + let icon; if (r.status === 'ok') { icon = color.green('\u2713'); @@ -1392,7 +1403,11 @@ class Installer { icon = color.red('\u2717'); } const detail = r.detail ? color.dim(` (${r.detail})`) : ''; - lines.push(` ${icon} ${r.step}${detail}`); + lines.push(` ${icon} ${stepLabel}${detail}`); + } + + if ((context.ides || []).length === 0) { + lines.push(` ${color.green('\u2713')} No IDE selected ${color.dim('(installed in _bmad only)')}`); } // Context and warnings @@ -1415,8 +1430,10 @@ class Installer { ` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`, ` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`, ` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`, - ` Invoke the ${color.cyan('bmad-help')} skill in your IDE Agent to get started`, ); + if (context.ides && context.ides.length > 0) { + lines.push(` Invoke the ${color.cyan('bmad-help')} skill in your IDE Agent to get started`); + } await prompts.note(lines.join('\n'), 'BMAD is ready to use!'); } diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 714aa752b..0abddd0dc 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -129,6 +129,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { const selectedModules = options.selectedModules || []; const results = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 }; + this.skillWriteTracker = config.skill_format ? new Set() : null; // Install standard artifacts (agents, workflows, tasks, tools) if (!skipStandardArtifacts) { @@ -159,9 +160,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { // Install verbatim skills (type: skill) if (config.skill_format) { results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config); + results.skillDirectories = this.skillWriteTracker ? this.skillWriteTracker.size : 0; } await this.printSummary(results, target_dir, options); + this.skillWriteTracker = null; return { success: true, results }; } @@ -495,6 +498,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} // Create skill directory const skillDir = path.join(targetPath, skillName); await this.ensureDir(skillDir); + this.skillWriteTracker?.add(skillName); // Transform content: rewrite frontmatter for skills format const skillContent = this.transformToSkillFormat(content, skillName); @@ -667,6 +671,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} const skillDir = path.join(targetPath, canonicalId); await fs.remove(skillDir); await fs.ensureDir(skillDir); + this.skillWriteTracker?.add(canonicalId); // Copy all skill files, filtering OS/editor artifacts recursively const skipPatterns = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']); @@ -707,11 +712,10 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} async printSummary(results, targetDir, options = {}) { if (options.silent) return; const parts = []; + const totalSkills = + results.skillDirectories || (results.workflows || 0) + (results.tasks || 0) + (results.tools || 0) + (results.skills || 0); + if (totalSkills > 0) parts.push(`${totalSkills} skills`); if (results.agents > 0) parts.push(`${results.agents} agents`); - if (results.workflows > 0) parts.push(`${results.workflows} workflows`); - if (results.tasks > 0) parts.push(`${results.tasks} tasks`); - if (results.tools > 0) parts.push(`${results.tools} tools`); - if (results.skills > 0) parts.push(`${results.skills} skills`); await prompts.log.success(`${this.name} configured: ${parts.join(', ')} → ${targetDir}`); } diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 2381bddfa..e5e13a202 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -162,10 +162,9 @@ class IdeManager { // Config-driven handlers return { success, results: { agents, workflows, tasks, tools } } const r = handlerResult.results; const parts = []; + const totalSkills = r.skillDirectories || (r.workflows || 0) + (r.tasks || 0) + (r.tools || 0) + (r.skills || 0); + if (totalSkills > 0) parts.push(`${totalSkills} skills`); if (r.agents > 0) parts.push(`${r.agents} agents`); - if (r.workflows > 0) parts.push(`${r.workflows} workflows`); - if (r.tasks > 0) parts.push(`${r.tasks} tasks`); - if (r.tools > 0) parts.push(`${r.tools} tools`); detail = parts.join(', '); } // Propagate handler's success status (default true for backward compat)