Compare commits

..

No commits in common. "ef3106461a875f873a7d935cf91bb9a0a474451a" and "5d7e42132e1b2348c0425a7f19c9e7ae4293a455" have entirely different histories.

9 changed files with 330 additions and 501 deletions

View File

@ -1,14 +0,0 @@
workflow-domain-research.md:
canonicalId: bmad-domain-research
type: workflow
description: "Conduct domain and industry research. Use when the user says 'lets create a research report on [domain or industry]'"
workflow-market-research.md:
canonicalId: bmad-market-research
type: workflow
description: "Conduct market research on competition and customers. Use when the user says 'create a market research report about [business idea]'"
workflow-technical-research.md:
canonicalId: bmad-technical-research
type: workflow
description: "Conduct technical research on technologies and architecture. Use when the user says 'create a technical research report on [topic]'"

View File

@ -1,14 +0,0 @@
workflow-create-prd.md:
canonicalId: bmad-create-prd
type: workflow
description: "Create a PRD from scratch. Use when the user says 'lets create a product requirements document' or 'I want to create a new PRD'"
workflow-edit-prd.md:
canonicalId: bmad-edit-prd
type: workflow
description: "Edit an existing PRD. Use when the user says 'edit this PRD'"
workflow-validate-prd.md:
canonicalId: bmad-validate-prd
type: workflow
description: "Validate a PRD against standards. Use when the user says 'validate this PRD' or 'run PRD validation'"

View File

@ -1135,334 +1135,82 @@ async function runTests() {
console.log(''); console.log('');
// ============================================================ // ============================================================
// Suite 22: KiloCoder Suspended // Suite 22: KiloCoder Native Skills
// ============================================================ // ============================================================
console.log(`${colors.yellow}Test Suite 22: KiloCoder Suspended${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 22: KiloCoder Native Skills${colors.reset}\n`);
try { try {
clearCache(); clearCache();
const platformCodes22 = await loadPlatformCodes(); const platformCodes22 = await loadPlatformCodes();
const kiloConfig22 = platformCodes22.platforms.kilo; const kiloInstaller = platformCodes22.platforms.kilo?.installer;
assert(typeof kiloConfig22?.suspended === 'string', 'KiloCoder has a suspended message in platform config'); assert(kiloInstaller?.target_dir === '.kilocode/skills', 'KiloCoder target_dir uses native skills path');
assert(kiloConfig22?.installer?.target_dir === '.kilocode/skills', 'KiloCoder retains target_dir config for future use'); assert(kiloInstaller?.skill_format === true, 'KiloCoder installer enables native skill output');
const ideManager22 = new IdeManager(); assert(
await ideManager22.ensureInitialized(); Array.isArray(kiloInstaller?.legacy_targets) && kiloInstaller.legacy_targets.includes('.kilocode/workflows'),
'KiloCoder installer cleans legacy workflows output',
);
// Should not appear in available IDEs // Fresh install test
const availableIdes22 = ideManager22.getAvailableIdes();
assert(!availableIdes22.some((ide) => ide.value === 'kilo'), 'KiloCoder is hidden from IDE selection');
// Setup should be blocked but legacy files should be cleaned up
const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-')); const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-'));
const installedBmadDir22 = await createTestBmadFixture(); const installedBmadDir22 = await createTestBmadFixture();
// Pre-populate legacy Kilo artifacts that should be cleaned up
const legacyDir22 = path.join(tempProjectDir22, '.kilocode', 'workflows'); const legacyDir22 = path.join(tempProjectDir22, '.kilocode', 'workflows');
await fs.ensureDir(legacyDir22); await fs.ensureDir(legacyDir22);
await fs.writeFile(path.join(legacyDir22, 'bmad-legacy.md'), 'legacy\n'); await fs.writeFile(path.join(legacyDir22, 'bmad-legacy.md'), 'legacy\n');
// Create a .kilocodemodes file with BMAD modes and a user mode
const kiloModesPath22 = path.join(tempProjectDir22, '.kilocodemodes');
const yaml22 = require('yaml');
const kiloModesContent = yaml22.stringify({
customModes: [
{ slug: 'bmad-bmm-architect', name: 'BMAD Architect', roleDefinition: 'test' },
{ slug: 'bmad-core-master', name: 'BMAD Master', roleDefinition: 'test' },
{ slug: 'user-custom-mode', name: 'My Custom Mode', roleDefinition: 'user mode' },
],
});
await fs.writeFile(kiloModesPath22, kiloModesContent);
const ideManager22 = new IdeManager();
await ideManager22.ensureInitialized();
const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, { const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
silent: true, silent: true,
selectedModules: ['bmm'], selectedModules: ['bmm'],
}); });
assert(result22.success === false, 'KiloCoder setup is blocked when suspended'); assert(result22.success === true, 'KiloCoder setup succeeds against temp project');
assert(result22.error === 'suspended', 'KiloCoder setup returns suspended error');
// Should not write new skill files const skillFile22 = path.join(tempProjectDir22, '.kilocode', 'skills', 'bmad-master', 'SKILL.md');
assert( assert(await fs.pathExists(skillFile22), 'KiloCoder install writes SKILL.md directory output');
!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'skills'))),
'KiloCoder does not create skills directory when suspended',
);
// Legacy files should be cleaned up const skillContent22 = await fs.readFile(skillFile22, 'utf8');
const nameMatch22 = skillContent22.match(/^name:\s*(.+)$/m);
assert(nameMatch22 && nameMatch22[1].trim() === 'bmad-master', 'KiloCoder skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'workflows'))), 'KiloCoder setup removes legacy workflows dir');
// Verify .kilocodemodes cleanup: BMAD modes removed, user mode preserved
const cleanedModes22 = yaml22.parse(await fs.readFile(kiloModesPath22, 'utf8'));
assert( assert(
!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'workflows'))), Array.isArray(cleanedModes22.customModes) && cleanedModes22.customModes.length === 1,
'KiloCoder legacy workflows are cleaned up even when suspended', 'KiloCoder cleanup removes BMAD modes from .kilocodemodes',
); );
assert(cleanedModes22.customModes[0].slug === 'user-custom-mode', 'KiloCoder cleanup preserves non-BMAD modes in .kilocodemodes');
// Reinstall test
const result22b = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
silent: true,
selectedModules: ['bmm'],
});
assert(result22b.success === true, 'KiloCoder reinstall/upgrade succeeds over existing skills');
assert(await fs.pathExists(skillFile22), 'KiloCoder reinstall preserves SKILL.md output');
await fs.remove(tempProjectDir22); await fs.remove(tempProjectDir22);
await fs.remove(installedBmadDir22); await fs.remove(installedBmadDir22);
} catch (error) { } catch (error) {
assert(false, 'KiloCoder suspended test succeeds', error.message); assert(false, 'KiloCoder native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Suite 23: Gemini CLI Native Skills
// ============================================================
console.log(`${colors.yellow}Test Suite 23: Gemini CLI Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes23 = await loadPlatformCodes();
const geminiInstaller = platformCodes23.platforms.gemini?.installer;
assert(geminiInstaller?.target_dir === '.gemini/skills', 'Gemini target_dir uses native skills path');
assert(geminiInstaller?.skill_format === true, 'Gemini installer enables native skill output');
assert(
Array.isArray(geminiInstaller?.legacy_targets) && geminiInstaller.legacy_targets.includes('.gemini/commands'),
'Gemini installer cleans legacy commands output',
);
const tempProjectDir23 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gemini-test-'));
const installedBmadDir23 = await createTestBmadFixture();
const legacyDir23 = path.join(tempProjectDir23, '.gemini', 'commands');
await fs.ensureDir(legacyDir23);
await fs.writeFile(path.join(legacyDir23, 'bmad-legacy.toml'), 'legacy\n');
const ideManager23 = new IdeManager();
await ideManager23.ensureInitialized();
const result23 = await ideManager23.setup('gemini', tempProjectDir23, installedBmadDir23, {
silent: true,
selectedModules: ['bmm'],
});
assert(result23.success === true, 'Gemini setup succeeds against temp project');
const skillFile23 = path.join(tempProjectDir23, '.gemini', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile23), 'Gemini install writes SKILL.md directory output');
const skillContent23 = await fs.readFile(skillFile23, 'utf8');
const nameMatch23 = skillContent23.match(/^name:\s*(.+)$/m);
assert(nameMatch23 && nameMatch23[1].trim() === 'bmad-master', 'Gemini skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir23, '.gemini', 'commands'))), 'Gemini setup removes legacy commands dir');
const result23b = await ideManager23.setup('gemini', tempProjectDir23, installedBmadDir23, {
silent: true,
selectedModules: ['bmm'],
});
assert(result23b.success === true, 'Gemini reinstall/upgrade succeeds over existing skills');
assert(await fs.pathExists(skillFile23), 'Gemini reinstall preserves SKILL.md output');
await fs.remove(tempProjectDir23);
await fs.remove(installedBmadDir23);
} catch (error) {
assert(false, 'Gemini native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Suite 24: iFlow Native Skills
// ============================================================
console.log(`${colors.yellow}Test Suite 24: iFlow Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes24 = await loadPlatformCodes();
const iflowInstaller = platformCodes24.platforms.iflow?.installer;
assert(iflowInstaller?.target_dir === '.iflow/skills', 'iFlow target_dir uses native skills path');
assert(iflowInstaller?.skill_format === true, 'iFlow installer enables native skill output');
assert(
Array.isArray(iflowInstaller?.legacy_targets) && iflowInstaller.legacy_targets.includes('.iflow/commands'),
'iFlow installer cleans legacy commands output',
);
const tempProjectDir24 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-iflow-test-'));
const installedBmadDir24 = await createTestBmadFixture();
const legacyDir24 = path.join(tempProjectDir24, '.iflow', 'commands');
await fs.ensureDir(legacyDir24);
await fs.writeFile(path.join(legacyDir24, 'bmad-legacy.md'), 'legacy\n');
const ideManager24 = new IdeManager();
await ideManager24.ensureInitialized();
const result24 = await ideManager24.setup('iflow', tempProjectDir24, installedBmadDir24, {
silent: true,
selectedModules: ['bmm'],
});
assert(result24.success === true, 'iFlow setup succeeds against temp project');
const skillFile24 = path.join(tempProjectDir24, '.iflow', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile24), 'iFlow install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir24, '.iflow', 'commands'))), 'iFlow setup removes legacy commands dir');
await fs.remove(tempProjectDir24);
await fs.remove(installedBmadDir24);
} catch (error) {
assert(false, 'iFlow native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Suite 25: QwenCoder Native Skills
// ============================================================
console.log(`${colors.yellow}Test Suite 25: QwenCoder Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes25 = await loadPlatformCodes();
const qwenInstaller = platformCodes25.platforms.qwen?.installer;
assert(qwenInstaller?.target_dir === '.qwen/skills', 'QwenCoder target_dir uses native skills path');
assert(qwenInstaller?.skill_format === true, 'QwenCoder installer enables native skill output');
assert(
Array.isArray(qwenInstaller?.legacy_targets) && qwenInstaller.legacy_targets.includes('.qwen/commands'),
'QwenCoder installer cleans legacy commands output',
);
const tempProjectDir25 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-qwen-test-'));
const installedBmadDir25 = await createTestBmadFixture();
const legacyDir25 = path.join(tempProjectDir25, '.qwen', 'commands');
await fs.ensureDir(legacyDir25);
await fs.writeFile(path.join(legacyDir25, 'bmad-legacy.md'), 'legacy\n');
const ideManager25 = new IdeManager();
await ideManager25.ensureInitialized();
const result25 = await ideManager25.setup('qwen', tempProjectDir25, installedBmadDir25, {
silent: true,
selectedModules: ['bmm'],
});
assert(result25.success === true, 'QwenCoder setup succeeds against temp project');
const skillFile25 = path.join(tempProjectDir25, '.qwen', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile25), 'QwenCoder install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir25, '.qwen', 'commands'))), 'QwenCoder setup removes legacy commands dir');
await fs.remove(tempProjectDir25);
await fs.remove(installedBmadDir25);
} catch (error) {
assert(false, 'QwenCoder native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Suite 26: Rovo Dev Native Skills
// ============================================================
console.log(`${colors.yellow}Test Suite 26: Rovo Dev Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes26 = await loadPlatformCodes();
const rovoInstaller = platformCodes26.platforms['rovo-dev']?.installer;
assert(rovoInstaller?.target_dir === '.rovodev/skills', 'Rovo Dev target_dir uses native skills path');
assert(rovoInstaller?.skill_format === true, 'Rovo Dev installer enables native skill output');
assert(
Array.isArray(rovoInstaller?.legacy_targets) && rovoInstaller.legacy_targets.includes('.rovodev/workflows'),
'Rovo Dev installer cleans legacy workflows output',
);
const tempProjectDir26 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-rovodev-test-'));
const installedBmadDir26 = await createTestBmadFixture();
const legacyDir26 = path.join(tempProjectDir26, '.rovodev', 'workflows');
await fs.ensureDir(legacyDir26);
await fs.writeFile(path.join(legacyDir26, 'bmad-legacy.md'), 'legacy\n');
// Create a prompts.yml with BMAD entries and a user entry
const yaml26 = require('yaml');
const promptsPath26 = path.join(tempProjectDir26, '.rovodev', 'prompts.yml');
const promptsContent26 = yaml26.stringify({
prompts: [
{ name: 'bmad-bmm-create-prd', description: 'BMAD workflow', content_file: 'workflows/bmad-bmm-create-prd.md' },
{ name: 'my-custom-prompt', description: 'User prompt', content_file: 'custom.md' },
],
});
await fs.writeFile(promptsPath26, promptsContent26);
const ideManager26 = new IdeManager();
await ideManager26.ensureInitialized();
const result26 = await ideManager26.setup('rovo-dev', tempProjectDir26, installedBmadDir26, {
silent: true,
selectedModules: ['bmm'],
});
assert(result26.success === true, 'Rovo Dev setup succeeds against temp project');
const skillFile26 = path.join(tempProjectDir26, '.rovodev', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile26), 'Rovo Dev install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir26, '.rovodev', 'workflows'))), 'Rovo Dev setup removes legacy workflows dir');
// Verify prompts.yml cleanup: BMAD entries removed, user entry preserved
const cleanedPrompts26 = yaml26.parse(await fs.readFile(promptsPath26, 'utf8'));
assert(
Array.isArray(cleanedPrompts26.prompts) && cleanedPrompts26.prompts.length === 1,
'Rovo Dev cleanup removes BMAD entries from prompts.yml',
);
assert(cleanedPrompts26.prompts[0].name === 'my-custom-prompt', 'Rovo Dev cleanup preserves non-BMAD entries in prompts.yml');
await fs.remove(tempProjectDir26);
await fs.remove(installedBmadDir26);
} catch (error) {
assert(false, 'Rovo Dev native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Suite 27: Cleanup preserves bmad-os-* skills
// ============================================================
console.log(`${colors.yellow}Test Suite 27: Cleanup preserves bmad-os-* skills${colors.reset}\n`);
try {
const tempProjectDir27 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-os-preserve-test-'));
const installedBmadDir27 = await createTestBmadFixture();
// Pre-populate .claude/skills with bmad-os-* skills (version-controlled repo skills)
const osSkillDir27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-os-review-pr');
await fs.ensureDir(osSkillDir27);
await fs.writeFile(
path.join(osSkillDir27, 'SKILL.md'),
'---\nname: bmad-os-review-pr\ndescription: Review PRs\n---\nOS skill content\n',
);
const osSkillDir27b = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-os-release-module');
await fs.ensureDir(osSkillDir27b);
await fs.writeFile(
path.join(osSkillDir27b, 'SKILL.md'),
'---\nname: bmad-os-release-module\ndescription: Release module\n---\nOS skill content\n',
);
// Also add a regular bmad skill that SHOULD be cleaned up
const regularSkillDir27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-architect');
await fs.ensureDir(regularSkillDir27);
await fs.writeFile(
path.join(regularSkillDir27, 'SKILL.md'),
'---\nname: bmad-architect\ndescription: Architect\n---\nOld skill content\n',
);
// Run Claude Code setup (which triggers cleanup then install)
const ideManager27 = new IdeManager();
await ideManager27.ensureInitialized();
const result27 = await ideManager27.setup('claude-code', tempProjectDir27, installedBmadDir27, {
silent: true,
selectedModules: ['bmm'],
});
assert(result27.success === true, 'Claude Code setup succeeds with bmad-os-* skills present');
// bmad-os-* skills must survive
assert(await fs.pathExists(osSkillDir27), 'Cleanup preserves bmad-os-review-pr skill');
assert(await fs.pathExists(osSkillDir27b), 'Cleanup preserves bmad-os-release-module skill');
// bmad-os skill content must be untouched
const osContent27 = await fs.readFile(path.join(osSkillDir27, 'SKILL.md'), 'utf8');
assert(osContent27.includes('OS skill content'), 'bmad-os-review-pr skill content is unchanged');
// Regular bmad skill should have been replaced by fresh install
const newSkillFile27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(newSkillFile27), 'Fresh bmad skills are installed alongside preserved bmad-os-* skills');
await fs.remove(tempProjectDir27);
await fs.remove(installedBmadDir27);
} catch (error) {
assert(false, 'bmad-os-* skill preservation test succeeds', error.message);
} }
console.log(''); console.log('');

View File

@ -717,25 +717,6 @@ class Installer {
config.skipIde = toolSelection.skipIde; config.skipIde = toolSelection.skipIde;
const ideConfigurations = toolSelection.configurations; const ideConfigurations = toolSelection.configurations;
// Early check: fail fast if ALL selected IDEs are suspended
if (config.ides && config.ides.length > 0) {
await this.ideManager.ensureInitialized();
const suspendedIdes = config.ides.filter((ide) => {
const handler = this.ideManager.handlers.get(ide);
return handler?.platformConfig?.suspended;
});
if (suspendedIdes.length > 0 && suspendedIdes.length === config.ides.length) {
for (const ide of suspendedIdes) {
const handler = this.ideManager.handlers.get(ide);
await prompts.log.error(`${handler.displayName || ide}: ${handler.platformConfig.suspended}`);
}
throw new Error(
`All selected tool(s) are suspended: ${suspendedIdes.join(', ')}. Installation aborted to prevent upgrading _bmad/ without a working IDE configuration.`,
);
}
}
// Detect IDEs that were previously installed but are NOT in the new selection (to be removed) // Detect IDEs that were previously installed but are NOT in the new selection (to be removed)
if (config._isUpdate && config._existingInstall) { if (config._isUpdate && config._existingInstall) {
const previouslyInstalledIdes = new Set(config._existingInstall.ides || []); const previouslyInstalledIdes = new Set(config._existingInstall.ides || []);

View File

@ -665,11 +665,6 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
await this.cleanupKiloModes(projectDir, options); await this.cleanupKiloModes(projectDir, options);
} }
// Strip BMAD entries from .rovodev/prompts.yml if present
if (this.name === 'rovo-dev') {
await this.cleanupRovoDevPrompts(projectDir, options);
}
// Clean all target directories // Clean all target directories
if (this.installerConfig?.targets) { if (this.installerConfig?.targets) {
const parentDirs = new Set(); const parentDirs = new Set();
@ -756,7 +751,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
if (!entry || typeof entry !== 'string') { if (!entry || typeof entry !== 'string') {
continue; continue;
} }
if (entry.startsWith('bmad') && !entry.startsWith('bmad-os-')) { if (entry.startsWith('bmad')) {
const entryPath = path.join(targetPath, entry); const entryPath = path.join(targetPath, entry);
try { try {
await fs.remove(entryPath); await fs.remove(entryPath);
@ -853,47 +848,6 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
} }
} }
/**
* Strip BMAD-owned entries from .rovodev/prompts.yml.
* The old custom rovodev.js installer registered workflows in prompts.yml.
* Parses YAML, filters out entries with name starting with 'bmad-', rewrites.
* Removes the file if no entries remain.
*/
async cleanupRovoDevPrompts(projectDir, options = {}) {
const promptsPath = path.join(projectDir, '.rovodev', 'prompts.yml');
if (!(await fs.pathExists(promptsPath))) return;
const content = await fs.readFile(promptsPath, 'utf8');
let config;
try {
config = yaml.parse(content) || {};
} catch {
if (!options.silent) await prompts.log.warn(' Warning: Could not parse prompts.yml for cleanup');
return;
}
if (!Array.isArray(config.prompts)) return;
const originalCount = config.prompts.length;
config.prompts = config.prompts.filter((entry) => entry && (!entry.name || !entry.name.startsWith('bmad-')));
const removedCount = originalCount - config.prompts.length;
if (removedCount > 0) {
try {
if (config.prompts.length === 0) {
await fs.remove(promptsPath);
} else {
await fs.writeFile(promptsPath, yaml.stringify(config, { lineWidth: 0 }));
}
if (!options.silent) await prompts.log.message(` Removed ${removedCount} BMAD entries from prompts.yml`);
} catch {
if (!options.silent) await prompts.log.warn(' Warning: Could not write prompts.yml during cleanup');
}
}
}
/** /**
* Check ancestor directories for existing BMAD files in the same target_dir. * Check ancestor directories for existing BMAD files in the same target_dir.
* IDEs like Claude Code inherit commands from parent directories, so an existing * IDEs like Claude Code inherit commands from parent directories, so an existing

View File

@ -8,8 +8,8 @@ const prompts = require('../../../lib/prompts');
* Dynamically discovers and loads IDE handlers * Dynamically discovers and loads IDE handlers
* *
* Loading strategy: * Loading strategy:
* All platforms are now config-driven from platform-codes.yaml. * 1. Custom installer files (rovodev.js) - for platforms with unique installation logic
* The custom installer file mechanism is retained for future use but currently has no entries. * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
*/ */
class IdeManager { class IdeManager {
constructor() { constructor() {
@ -58,11 +58,11 @@ class IdeManager {
/** /**
* Load custom installer files (unique installation logic) * Load custom installer files (unique installation logic)
* These files have special installation patterns that don't fit the config-driven model * These files have special installation patterns that don't fit the config-driven model
* Note: All custom installers (codex, github-copilot, kilo, rovodev) have been migrated to config-driven (platform-codes.yaml) * Note: codex, github-copilot, and kilo were migrated to config-driven (platform-codes.yaml)
*/ */
async loadCustomInstallerFiles() { async loadCustomInstallerFiles() {
const ideDir = __dirname; const ideDir = __dirname;
const customFiles = []; const customFiles = ['rovodev.js'];
for (const file of customFiles) { for (const file of customFiles) {
const filePath = path.join(ideDir, file); const filePath = path.join(ideDir, file);
@ -128,11 +128,6 @@ class IdeManager {
continue; continue;
} }
// Skip suspended platforms (e.g., IDE doesn't support skills yet)
if (handler.platformConfig?.suspended) {
continue;
}
ides.push({ ides.push({
value: key, value: key,
name: name, name: name,
@ -182,18 +177,6 @@ class IdeManager {
return { success: false, ide: ideName, error: 'unsupported IDE' }; return { success: false, ide: ideName, error: 'unsupported IDE' };
} }
// Block suspended platforms — clean up legacy files but don't install
if (handler.platformConfig?.suspended) {
if (!options.silent) {
await prompts.log.warn(`${handler.displayName || ideName}: ${handler.platformConfig.suspended}`);
}
// Still clean up legacy artifacts so old broken configs don't linger
if (typeof handler.cleanup === 'function') {
await handler.cleanup(projectDir, { silent: true });
}
return { success: false, ide: ideName, error: 'suspended' };
}
try { try {
const handlerResult = await handler.setup(projectDir, bmadDir, options); const handlerResult = await handler.setup(projectDir, bmadDir, options);
// Build detail string from handler-returned data // Build detail string from handler-returned data

View File

@ -120,11 +120,8 @@ platforms:
category: cli category: cli
description: "Google's CLI for Gemini" description: "Google's CLI for Gemini"
installer: installer:
legacy_targets: target_dir: .gemini/commands
- .gemini/commands template_type: gemini
target_dir: .gemini/skills
template_type: default
skill_format: true
github-copilot: github-copilot:
name: "GitHub Copilot" name: "GitHub Copilot"
@ -145,18 +142,14 @@ platforms:
category: ide category: ide
description: "AI workflow automation" description: "AI workflow automation"
installer: installer:
legacy_targets: target_dir: .iflow/commands
- .iflow/commands
target_dir: .iflow/skills
template_type: default template_type: default
skill_format: true
kilo: kilo:
name: "KiloCoder" name: "KiloCoder"
preferred: false preferred: false
category: ide category: ide
description: "AI coding platform" description: "AI coding platform"
suspended: "Kilo Code does not yet support the Agent Skills standard. Support is paused until they implement it. See https://github.com/kilocode/kilo-code/issues for updates."
installer: installer:
legacy_targets: legacy_targets:
- .kilocode/workflows - .kilocode/workflows
@ -198,11 +191,8 @@ platforms:
category: ide category: ide
description: "Qwen AI coding assistant" description: "Qwen AI coding assistant"
installer: installer:
legacy_targets: target_dir: .qwen/commands
- .qwen/commands
target_dir: .qwen/skills
template_type: default template_type: default
skill_format: true
roo: roo:
name: "Roo Cline" name: "Roo Cline"
@ -221,12 +211,7 @@ platforms:
preferred: false preferred: false
category: ide category: ide
description: "Atlassian's Rovo development environment" description: "Atlassian's Rovo development environment"
installer: # No installer config - uses custom rovodev.js (generates prompts.yml manifest)
legacy_targets:
- .rovodev/workflows
target_dir: .rovodev/skills
template_type: default
skill_format: true
trae: trae:
name: "Trae" name: "Trae"

View File

@ -0,0 +1,257 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
const { BaseIdeSetup } = require('./_base-ide');
const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
const { toDashPath } = require('./shared/path-utils');
/**
* Rovo Dev IDE setup handler
*
* Custom installer that writes .md workflow files to .rovodev/workflows/
* and generates .rovodev/prompts.yml to register them with Rovo Dev's /prompts feature.
*
* prompts.yml format (per Rovo Dev docs):
* prompts:
* - name: bmad-bmm-create-prd
* description: "PRD workflow..."
* content_file: workflows/bmad-bmm-create-prd.md
*/
class RovoDevSetup extends BaseIdeSetup {
constructor() {
super('rovo-dev', 'Rovo Dev', false);
this.rovoDir = '.rovodev';
this.workflowsDir = 'workflows';
this.promptsFile = 'prompts.yml';
}
/**
* Setup Rovo Dev configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
* @returns {Promise<Object>} Setup result with { success, results: { agents, workflows, tasks, tools } }
*/
async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
// Clean up any old BMAD installation first
await this.cleanup(projectDir, options);
const workflowsPath = path.join(projectDir, this.rovoDir, this.workflowsDir);
await this.ensureDir(workflowsPath);
const selectedModules = options.selectedModules || [];
const writtenFiles = [];
// Generate and write agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
const agentCount = await agentGen.writeDashArtifacts(workflowsPath, agentArtifacts);
this._collectPromptEntries(writtenFiles, agentArtifacts, ['agent-launcher'], 'agent');
// Generate and write workflow commands
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
const workflowCount = await workflowGen.writeDashArtifacts(workflowsPath, workflowArtifacts);
this._collectPromptEntries(writtenFiles, workflowArtifacts, ['workflow-command'], 'workflow');
// Generate and write task/tool commands
const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
await taskToolGen.writeDashArtifacts(workflowsPath, taskToolArtifacts);
const taskCount = taskToolCounts.tasks || 0;
const toolCount = taskToolCounts.tools || 0;
this._collectPromptEntries(writtenFiles, taskToolArtifacts, ['task', 'tool']);
// Generate prompts.yml manifest (only if we have entries to write)
if (writtenFiles.length > 0) {
await this.generatePromptsYml(projectDir, writtenFiles);
}
if (!options.silent) {
await prompts.log.success(
`${this.name} configured: ${agentCount} agents, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools`,
);
}
return {
success: true,
results: {
agents: agentCount,
workflows: workflowCount,
tasks: taskCount,
tools: toolCount,
},
};
}
/**
* Collect prompt entries from artifacts into writtenFiles array
* @param {Array} writtenFiles - Target array to push entries into
* @param {Array} artifacts - Artifacts from a generator's collect method
* @param {string[]} acceptedTypes - Artifact types to include (e.g., ['agent-launcher'])
* @param {string} [fallbackSuffix] - Suffix for fallback description; defaults to artifact.type
*/
_collectPromptEntries(writtenFiles, artifacts, acceptedTypes, fallbackSuffix) {
for (const artifact of artifacts) {
if (!acceptedTypes.includes(artifact.type)) continue;
const flatName = toDashPath(artifact.relativePath);
writtenFiles.push({
name: path.basename(flatName, '.md'),
description: artifact.description || `${artifact.name} ${fallbackSuffix || artifact.type}`,
contentFile: `${this.workflowsDir}/${flatName}`,
});
}
}
/**
* Generate .rovodev/prompts.yml manifest
* Merges with existing user entries -- strips entries with names starting 'bmad-',
* appends new BMAD entries, and writes back.
*
* @param {string} projectDir - Project directory
* @param {Array<Object>} writtenFiles - Array of { name, description, contentFile }
*/
async generatePromptsYml(projectDir, writtenFiles) {
const promptsPath = path.join(projectDir, this.rovoDir, this.promptsFile);
let existingPrompts = [];
// Read existing prompts.yml and preserve non-BMAD entries
if (await this.pathExists(promptsPath)) {
try {
const content = await this.readFile(promptsPath);
const parsed = yaml.parse(content);
if (parsed && Array.isArray(parsed.prompts)) {
// Keep only non-BMAD entries (entries whose name does NOT start with bmad-)
existingPrompts = parsed.prompts.filter((entry) => !entry.name || !entry.name.startsWith('bmad-'));
}
} catch {
// If parsing fails, start fresh but preserve file safety
existingPrompts = [];
}
}
// Build new BMAD entries (prefix description with name so /prompts list is scannable)
const bmadEntries = writtenFiles.map((file) => ({
name: file.name,
description: `${file.name} - ${file.description}`,
content_file: file.contentFile,
}));
// Merge: user entries first, then BMAD entries
const allPrompts = [...existingPrompts, ...bmadEntries];
const config = { prompts: allPrompts };
const yamlContent = yaml.stringify(config, { lineWidth: 0 });
await this.writeFile(promptsPath, yamlContent);
}
/**
* Cleanup Rovo Dev configuration
* Removes bmad-* files from .rovodev/workflows/ and strips BMAD entries from prompts.yml
* @param {string} projectDir - Project directory
* @param {Object} options - Cleanup options
*/
async cleanup(projectDir, options = {}) {
const workflowsPath = path.join(projectDir, this.rovoDir, this.workflowsDir);
// Remove all bmad-* entries from workflows dir (aligned with detect() predicate)
if (await this.pathExists(workflowsPath)) {
const entries = await fs.readdir(workflowsPath);
for (const entry of entries) {
if (entry.startsWith('bmad-')) {
await fs.remove(path.join(workflowsPath, entry));
}
}
}
// Clean BMAD entries from prompts.yml (preserve user entries)
const promptsPath = path.join(projectDir, this.rovoDir, this.promptsFile);
if (await this.pathExists(promptsPath)) {
try {
const content = await this.readFile(promptsPath);
const parsed = yaml.parse(content) || {};
if (Array.isArray(parsed.prompts)) {
const originalCount = parsed.prompts.length;
parsed.prompts = parsed.prompts.filter((entry) => !entry.name || !entry.name.startsWith('bmad-'));
const removedCount = originalCount - parsed.prompts.length;
if (removedCount > 0) {
if (parsed.prompts.length === 0) {
// If no entries remain, remove the file entirely
await fs.remove(promptsPath);
} else {
await this.writeFile(promptsPath, yaml.stringify(parsed, { lineWidth: 0 }));
}
if (!options.silent) {
await prompts.log.message(`Removed ${removedCount} BMAD entries from ${this.promptsFile}`);
}
}
}
} catch {
// If parsing fails, leave file as-is
if (!options.silent) {
await prompts.log.warn(`Warning: Could not parse ${this.promptsFile} for cleanup`);
}
}
}
// Remove empty .rovodev directories
if (await this.pathExists(workflowsPath)) {
const remaining = await fs.readdir(workflowsPath);
if (remaining.length === 0) {
await fs.remove(workflowsPath);
}
}
const rovoDirPath = path.join(projectDir, this.rovoDir);
if (await this.pathExists(rovoDirPath)) {
const remaining = await fs.readdir(rovoDirPath);
if (remaining.length === 0) {
await fs.remove(rovoDirPath);
}
}
}
/**
* Detect whether Rovo Dev configuration exists in the project
* Checks for .rovodev/ dir with bmad files or bmad entries in prompts.yml
* @param {string} projectDir - Project directory
* @returns {boolean}
*/
async detect(projectDir) {
const workflowsPath = path.join(projectDir, this.rovoDir, this.workflowsDir);
// Check for bmad files in workflows dir
if (await fs.pathExists(workflowsPath)) {
const entries = await fs.readdir(workflowsPath);
if (entries.some((entry) => entry.startsWith('bmad-'))) {
return true;
}
}
// Check for bmad entries in prompts.yml
const promptsPath = path.join(projectDir, this.rovoDir, this.promptsFile);
if (await fs.pathExists(promptsPath)) {
try {
const content = await fs.readFile(promptsPath, 'utf8');
const parsed = yaml.parse(content);
if (parsed && Array.isArray(parsed.prompts)) {
return parsed.prompts.some((entry) => entry.name && entry.name.startsWith('bmad-'));
}
} catch {
// If parsing fails, check raw content
return false;
}
}
return false;
}
}
module.exports = { RovoDevSetup };

View File

@ -129,7 +129,7 @@ Support assumption: full Agent Skills support. Crush scans project-local `.crush
- [x] Test fresh install — 43 skills installed to `.crush/skills/` - [x] Test fresh install — 43 skills installed to `.crush/skills/`
- [x] Test reinstall/upgrade from legacy command output - [x] Test reinstall/upgrade from legacy command output
- [x] Confirm no ancestor conflict protection is needed because Crush only scans project-local `.crush/skills/`, no ancestor inheritance - [x] Confirm no ancestor conflict protection is needed because Crush only scans project-local `.crush/skills/`, no ancestor inheritance
- [x] Manual CLI verification — `crush run` lists all 10 skills and successfully triggers bmad-help - [ ] **NEEDS MANUAL IDE VERIFICATION** — install Crush via brew and confirm skills appear in UI
- [x] Implement/extend automated tests — 9 assertions in test suite 20 - [x] Implement/extend automated tests — 9 assertions in test suite 20
- [x] Commit - [x] Commit
@ -205,77 +205,26 @@ Support assumption: full Agent Skills support. BMAD currently uses a custom inst
- [x] Implement/extend automated tests — 11 assertions in test suite 17 including marker cleanup - [x] Implement/extend automated tests — 11 assertions in test suite 17 including marker cleanup
- [x] Commit - [x] Commit
## KiloCoder — SUSPENDED ## KiloCoder
**Status: Kilo Code does not support the Agent Skills standard.** The original migration assumed skills support because Kilo forked from Roo Code, but manual IDE verification confirmed Kilo has not merged that feature. BMAD support is paused until Kilo implements skills. Support assumption: full Agent Skills support. BMAD currently uses a custom installer that writes `.kilocodemodes` and `.kilocode/workflows`; target should move to native skills output.
**Install:** VS Code extension `kilocode.kilo-code` — search "Kilo Code" in Extensions or `code --install-extension kilocode.kilo-code` **Install:** VS Code extension `kilocode.kilo-code` — search "Kilo Code" in Extensions or `code --install-extension kilocode.kilo-code`
- [x] ~~Confirm KiloCoder native skills path~~**FALSE**: assumed from Roo Code fork, not verified. Manual testing showed no skills support in the IDE - [x] Confirm KiloCoder native skills path is `.kilocode/skills/{skill-name}/SKILL.md` (Kilo forked from Roo Code which uses `.roo/skills/`)
- [x] Config and installer code retained in platform-codes.yaml with `suspended` flag — hidden from IDE picker, setup blocked with explanation - [x] Design the migration away from modes plus workflow markdown — replaced 269-line custom kilo.js with config-driven installer entry in platform-codes.yaml
- [x] Installer fails early (before writing `_bmad/`) if Kilo is the only selected IDE, protecting existing installations - [x] Implement native skills output — target_dir `.kilocode/skills`, skill_format true, template_type default
- [x] Legacy cleanup still runs for `.kilocode/workflows` and `.kilocodemodes` when users switch to a different IDE - [x] Add legacy cleanup for `.kilocode/workflows` (via legacy_targets) and BMAD-owned entries in `.kilocodemodes` (via `cleanupKiloModes()` in `_config-driven.js`, same pattern as `copilot-instructions.md` cleanup)
- [x] Automated tests — 7 assertions in suite 22 (suspended config, hidden from picker, setup blocked, no files written, legacy cleanup) - [x] Test fresh install — skills written to `.kilocode/skills/bmad-master/SKILL.md` with correct frontmatter
- [x] Test reinstall/upgrade from legacy custom installer output — legacy workflows removed, skills installed
## Gemini CLI - [x] Confirm no ancestor conflict protection is needed — Kilo Code (like Cline) only scans workspace-local `.kilocode/skills/`, no ancestor directory inheritance
- [x] Implement/extend automated tests — 11 assertions in test suite 22 (config, fresh install, legacy cleanup, .kilocodemodes cleanup, reinstall)
Support assumption: full Agent Skills support. Gemini CLI docs confirm workspace skills at `.gemini/skills/` and user skills at `~/.gemini/skills/`. Also discovers `.agents/skills/` as an alias. BMAD previously installed TOML files to `.gemini/commands`.
**Install:** `npm install -g @anthropic-ai/gemini-cli` or see [geminicli.com](https://geminicli.com)
- [x] Confirm Gemini CLI native skills path is `.gemini/skills/{skill-name}/SKILL.md` (per [geminicli.com/docs/cli/skills](https://geminicli.com/docs/cli/skills/))
- [x] Implement native skills output — target_dir `.gemini/skills`, skill_format true, template_type default (replaces TOML templates)
- [x] Add legacy cleanup for `.gemini/commands` (via `legacy_targets`)
- [x] Test fresh install — skills written to `.gemini/skills/bmad-master/SKILL.md` with correct frontmatter
- [x] Test reinstall/upgrade from legacy TOML command output — legacy dir removed, skills installed
- [x] Confirm no ancestor conflict protection is needed — Gemini CLI uses workspace > user > extension precedence, no ancestor directory inheritance
- [x] Implement/extend automated tests — 9 assertions in test suite 23 (config, fresh install, legacy cleanup, reinstall)
- [x] Manual CLI verification — `gemini` lists all 10 skills and successfully triggers bmad-help
- [ ] Commit
## iFlow
Support assumption: full Agent Skills support. iFlow docs confirm workspace skills at `.iflow/skills/` and global skills at `~/.iflow/skills/`. BMAD previously installed flat files to `.iflow/commands`.
- [x] Confirm iFlow native skills path is `.iflow/skills/{skill-name}/SKILL.md`
- [x] Implement native skills output — target_dir `.iflow/skills`, skill_format true, template_type default
- [x] Add legacy cleanup for `.iflow/commands` (via `legacy_targets`)
- [x] Test fresh install — skills written to `.iflow/skills/bmad-master/SKILL.md`
- [x] Test legacy cleanup — legacy commands dir removed
- [x] Implement/extend automated tests — 6 assertions in test suite 24
- [ ] **NEEDS MANUAL IDE VERIFICATION** — install iFlow and confirm skills appear in UI and can be triggered
- [ ] Commit
## QwenCoder
Support assumption: full Agent Skills support. Qwen Code supports workspace skills at `.qwen/skills/` and global skills at `~/.qwen/skills/`. BMAD previously installed flat files to `.qwen/commands`.
- [x] Confirm QwenCoder native skills path is `.qwen/skills/{skill-name}/SKILL.md`
- [x] Implement native skills output — target_dir `.qwen/skills`, skill_format true, template_type default
- [x] Add legacy cleanup for `.qwen/commands` (via `legacy_targets`)
- [x] Test fresh install — skills written to `.qwen/skills/bmad-master/SKILL.md`
- [x] Test legacy cleanup — legacy commands dir removed
- [x] Implement/extend automated tests — 6 assertions in test suite 25
- [ ] **NEEDS MANUAL IDE VERIFICATION** — install QwenCoder and confirm skills appear in UI and can be triggered
- [ ] Commit
## Rovo Dev
Support assumption: full Agent Skills support. Rovo Dev now supports workspace skills at `.rovodev/skills/` and user skills at `~/.rovodev/skills/`. BMAD previously used a custom 257-line installer that wrote `.rovodev/workflows/` and `prompts.yml`.
- [x] Confirm Rovo Dev native skills path is `.rovodev/skills/{skill-name}/SKILL.md` (per Atlassian blog)
- [x] Replace 257-line custom `rovodev.js` with config-driven entry in `platform-codes.yaml`
- [x] Add legacy cleanup for `.rovodev/workflows` (via `legacy_targets`) and BMAD entries in `prompts.yml` (via `cleanupRovoDevPrompts()` in `_config-driven.js`)
- [x] Test fresh install — skills written to `.rovodev/skills/bmad-master/SKILL.md`
- [x] Test legacy cleanup — legacy workflows dir removed, `prompts.yml` BMAD entries stripped while preserving user entries
- [x] Implement/extend automated tests — 8 assertions in test suite 26
- [ ] **NEEDS MANUAL IDE VERIFICATION** — install Rovo Dev and confirm skills appear in UI and can be triggered
- [ ] Commit - [ ] Commit
## Summary Gates ## Summary Gates
- [x] All full-support BMAD platforms install `SKILL.md` directory-based output - [ ] All full-support BMAD platforms install `SKILL.md` directory-based output
- [x] No full-support platform still emits BMAD command/workflow/rule files as its primary install format - [ ] No full-support platform still emits BMAD command/workflow/rule files as its primary install format
- [x] Legacy cleanup paths are defined for every migrated platform - [ ] Legacy cleanup paths are defined for every migrated platform
- [x] Automated coverage exists for config-driven and custom-installer migrations - [ ] Automated coverage exists for config-driven and custom-installer migrations
- [ ] Installer docs and migration notes updated after code changes land - [ ] Installer docs and migration notes updated after code changes land