Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions test/test-installation-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,60 @@ async function createTestBmadFixture() {
return fixtureDir;
}

async function createSkillCollisionFixture() {
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-skill-collision-'));
Comment thread
alexeyv marked this conversation as resolved.
const fixtureDir = path.join(fixtureRoot, '_bmad');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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
*/
Expand Down Expand Up @@ -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(() => {});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

console.log('');

// ============================================================
// Summary
// ============================================================
Expand Down
33 changes: 25 additions & 8 deletions tools/cli/installers/lib/core/installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand All @@ -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)')}`);
Comment thread
alexeyv marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Context and warnings
Expand All @@ -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!');
}
Expand Down
12 changes: 8 additions & 4 deletions tools/cli/installers/lib/ide/_config-driven.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 };
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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']);
Expand Down Expand Up @@ -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}`);
}

Expand Down
5 changes: 2 additions & 3 deletions tools/cli/installers/lib/ide/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading