fix(installer): simplify install summary (#1915)

* fix(installer): simplify install summary

* style: fix prettier formatting in test file

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(installer): clean up temp dir leak and conditional IDE footer

- Return fixture root from createSkillCollisionFixture so cleanup
  removes the parent temp directory, not just the _bmad child
- Only show bmad-help next-step line when IDEs are configured

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Verkhovsky 2026-03-12 08:39:10 -06:00 committed by GitHub
parent 861716fbe3
commit c57506464f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 133 additions and 15 deletions

View File

@ -81,6 +81,60 @@ async function createTestBmadFixture() {
return fixtureDir; 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', '---', '', '<agent name="BMAD Master" title="Master">', '</agent>'].join(
'\n',
),
);
return { root: fixtureRoot, bmadDir: fixtureDir };
}
/** /**
* Test Suite * Test Suite
*/ */
@ -1770,6 +1824,50 @@ async function runTests() {
console.log(''); 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 // Summary
// ============================================================ // ============================================================

View File

@ -1153,12 +1153,6 @@ class Installer {
preservedModules: modulesForCsvPreserve, preservedModules: modulesForCsvPreserve,
}); });
addResult(
'Manifests',
'ok',
`${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`,
);
// Merge help catalogs // Merge help catalogs
message('Generating help catalog...'); message('Generating help catalog...');
await this.mergeModuleHelpCatalogs(bmadDir); await this.mergeModuleHelpCatalogs(bmadDir);
@ -1379,10 +1373,27 @@ class Installer {
*/ */
async renderInstallSummary(results, context = {}) { async renderInstallSummary(results, context = {}) {
const color = await prompts.getColor(); const color = await prompts.getColor();
const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
// Build step lines with status indicators // Build step lines with status indicators
const lines = []; const lines = [];
for (const r of results) { 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; let icon;
if (r.status === 'ok') { if (r.status === 'ok') {
icon = color.green('\u2713'); icon = color.green('\u2713');
@ -1392,7 +1403,11 @@ class Installer {
icon = color.red('\u2717'); icon = color.red('\u2717');
} }
const detail = r.detail ? color.dim(` (${r.detail})`) : ''; 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 // Context and warnings
@ -1415,8 +1430,10 @@ class Installer {
` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`, ` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`, ` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`,
` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`, ` 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!'); await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
} }

View File

@ -129,6 +129,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const selectedModules = options.selectedModules || []; const selectedModules = options.selectedModules || [];
const results = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 }; 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) // Install standard artifacts (agents, workflows, tasks, tools)
if (!skipStandardArtifacts) { if (!skipStandardArtifacts) {
@ -159,9 +160,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
// Install verbatim skills (type: skill) // Install verbatim skills (type: skill)
if (config.skill_format) { if (config.skill_format) {
results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config); results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config);
results.skillDirectories = this.skillWriteTracker ? this.skillWriteTracker.size : 0;
} }
await this.printSummary(results, target_dir, options); await this.printSummary(results, target_dir, options);
this.skillWriteTracker = null;
return { success: true, results }; return { success: true, results };
} }
@ -495,6 +498,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
// Create skill directory // Create skill directory
const skillDir = path.join(targetPath, skillName); const skillDir = path.join(targetPath, skillName);
await this.ensureDir(skillDir); await this.ensureDir(skillDir);
this.skillWriteTracker?.add(skillName);
// Transform content: rewrite frontmatter for skills format // Transform content: rewrite frontmatter for skills format
const skillContent = this.transformToSkillFormat(content, skillName); const skillContent = this.transformToSkillFormat(content, skillName);
@ -667,6 +671,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
const skillDir = path.join(targetPath, canonicalId); const skillDir = path.join(targetPath, canonicalId);
await fs.remove(skillDir); await fs.remove(skillDir);
await fs.ensureDir(skillDir); await fs.ensureDir(skillDir);
this.skillWriteTracker?.add(canonicalId);
// Copy all skill files, filtering OS/editor artifacts recursively // Copy all skill files, filtering OS/editor artifacts recursively
const skipPatterns = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']); 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 = {}) { async printSummary(results, targetDir, options = {}) {
if (options.silent) return; if (options.silent) return;
const parts = []; 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.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}`); await prompts.log.success(`${this.name} configured: ${parts.join(', ')}${targetDir}`);
} }

View File

@ -162,10 +162,9 @@ class IdeManager {
// Config-driven handlers return { success, results: { agents, workflows, tasks, tools } } // Config-driven handlers return { success, results: { agents, workflows, tasks, tools } }
const r = handlerResult.results; const r = handlerResult.results;
const parts = []; 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.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(', '); detail = parts.join(', ');
} }
// Propagate handler's success status (default true for backward compat) // Propagate handler's success status (default true for backward compat)