Compare commits

..

17 Commits

Author SHA1 Message Date
Alex Verkhovsky ef3106461a fix(skills): add canonicalIds for BMM research and PRD workflows
Drop the bmm module prefix from 6 workflow skill names so they
install as bmad-create-prd, bmad-domain-research, etc. instead of
bmad-bmm-create-prd, bmad-bmm-domain-research, etc.
2026-03-07 11:11:47 -07:00
Alex Verkhovsky e530a94d43 docs: update KiloCoder checklist to reflect suspended status 2026-03-07 05:59:31 -07:00
Alex Verkhovsky bf0b248a62 fix(installer): suspend Kilo Code and add verified Gemini/Crush results
Kilo Code does not support the Agent Skills standard — the migration
from modes+workflows to skills was based on a false fork assumption.

- Add suspended field to platform-codes.yaml, hiding Kilo from the IDE
  picker and blocking setup with a clear message
- Fail the installer early (before writing _bmad/) if all selected IDEs
  are suspended, protecting existing installations from being corrupted
- Still clean up legacy Kilo artifacts (.kilocodemodes, .kilocode/workflows)
  when users switch to a different IDE
- Mark Crush and Gemini CLI as manually verified (both work end-to-end)
- Replace Suite 22 install tests with suspended-behavior tests (7 assertions)
2026-03-07 05:56:38 -07:00
Alex Verkhovsky 5113e29fc4 docs: flag all unverified platforms for manual IDE testing
Add NEEDS MANUAL IDE VERIFICATION to KiloCoder, Gemini CLI, iFlow,
QwenCoder, and Rovo Dev checklists. CodeBuddy, Crush, and Trae already
had the flag.
2026-03-07 05:05:19 -07:00
Alex Verkhovsky 140cbb7b99 fix(installer): preserve bmad-os-* skills during cleanup
The cleanupTarget method removed all entries starting with "bmad" from
IDE skills directories, which would also wipe version-controlled
bmad-os-* skills from the BMAD-METHOD repo. Add exclusion for the
bmad-os- prefix so those skills survive reinstalls.
2026-03-07 04:18:37 -07:00
Alex Verkhovsky cd3432c099 feat(skills): migrate iFlow, QwenCoder, and Rovo Dev to native skills
Complete the native skills migration for all remaining platforms:

- iFlow: .iflow/commands → .iflow/skills (config change)
- QwenCoder: .qwen/commands → .qwen/skills (config change)
- Rovo Dev: replace 257-line custom rovodev.js with config-driven
  .rovodev/skills, add cleanupRovoDevPrompts() for prompts.yml cleanup

All platforms now use config-driven native skills. No custom installer
files remain. Manager.js customFiles array is now empty.

- Add test suites 24-26: 20 new assertions (173 total)
- Update migration checklist: all summary gates passed
- Delete tools/cli/installers/lib/ide/rovodev.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 04:00:21 -07:00
Alex Verkhovsky b7381d283e feat(skills): migrate Gemini CLI to config-driven native skills
Replace TOML-based .gemini/commands output with native SKILL.md output
in .gemini/skills/. Gemini CLI confirms native skills support per
geminicli.com/docs/cli/skills/.

- Update platform-codes.yaml: target_dir, skill_format, legacy_targets
- Add test Suite 23: 9 assertions (config, install, legacy, reinstall)
- Add Gemini CLI section to migration checklist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 03:56:39 -07:00
Alex Verkhovsky 5d7e42132e feat(skills): migrate KiloCoder to config-driven native skills
Replace 269-line custom kilo.js installer with config-driven entry in
platform-codes.yaml targeting .kilocode/skills/ with skill_format: true.

- Add installer config: target_dir, skill_format, template_type, legacy_targets
- Add cleanupKiloModes() to strip BMAD modes from .kilocodemodes on cleanup
- Remove kilo.js from manager.js customFiles and Kilo-specific result handling
- Delete tools/cli/installers/lib/ide/kilo.js
- Add test Suite 22: 11 assertions (config, install, legacy cleanup, modes, reinstall)
- Update migration checklist with verified results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:34:35 -07:00
Alex Verkhovsky 2c62cee013 feat(skills): migrate Trae to config-driven native skills
Move Trae installer from .trae/rules to .trae/skills with SKILL.md
directory output. Add legacy cleanup and 9 test assertions.
2026-03-07 00:19:10 -07:00
Alex Verkhovsky 3d96cd7f89 feat(skills): migrate Crush to config-driven native skills
Move Crush installer from .crush/commands to .crush/skills with
SKILL.md directory output. Add legacy cleanup and 9 test assertions.
2026-03-07 00:17:53 -07:00
Alex Verkhovsky 5e86816276 feat(skills): migrate CodeBuddy to config-driven native skills
Move CodeBuddy installer from .codebuddy/commands to .codebuddy/skills
with SKILL.md directory output. Add legacy cleanup and 9 test assertions.
2026-03-07 00:15:06 -07:00
Alex Verkhovsky 11d1ea1aa3 feat(skills): migrate Cline to config-driven native skills
Move Cline installer from .clinerules/workflows to .cline/skills with
SKILL.md directory output. Add legacy cleanup and 9 test assertions.
2026-03-06 23:54:40 -07:00
Alex Verkhovsky 10b789aa6f docs: update migration checklist with Copilot and Roo verified results
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:30:36 -07:00
Alex Verkhovsky 7a24098de1 feat(skills): migrate GitHub Copilot to config-driven native skills
Replace 699-line custom installer with config-driven skill_format.
Output moves from .github/agents/ + .github/prompts/ to
.github/skills/{skill-name}/SKILL.md. Legacy cleanup strips BMAD
markers from copilot-instructions.md and removes old directories.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:16:14 -07:00
Alex Verkhovsky 9d5d6b48d1 test(skills): add Roo Code reinstall/upgrade test
Verify that running Roo setup over existing skills output succeeds
and preserves SKILL.md output. Checks off the last Roo checklist item.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:12:40 -07:00
Alex Verkhovsky 90ed20754a test(skills): add native skills tests for Claude Code, Codex, and Cursor
Add dedicated test suites covering config validation, fresh install,
legacy cleanup, and ancestor conflict detection for Claude Code, Codex
CLI, and Cursor. Updates migration checklist to reflect verified status.

84 assertions now pass (up from 50).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:07:39 -07:00
Alex Verkhovsky d95fa4c14f feat(skills): migrate Roo Code installer to native skills format
Move Roo Code from legacy `.roo/commands/` flat files to native
`.roo/skills/{skill-name}/SKILL.md` directory output. Verified
skill discovery in Roo Code v3.51 with 43 skills installed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:00:07 -07:00
7 changed files with 69 additions and 82 deletions

View File

@ -495,11 +495,6 @@ async function runTests() {
const skillFile9 = path.join(tempProjectDir9, '.claude', 'skills', 'bmad-master', 'SKILL.md'); const skillFile9 = path.join(tempProjectDir9, '.claude', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile9), 'Claude Code install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile9), 'Claude Code install writes SKILL.md directory output');
// Verify name frontmatter matches directory name
const skillContent9 = await fs.readFile(skillFile9, 'utf8');
const nameMatch9 = skillContent9.match(/^name:\s*(.+)$/m);
assert(nameMatch9 && nameMatch9[1].trim() === 'bmad-master', 'Claude Code skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(legacyDir9)), 'Claude Code setup removes legacy commands dir'); assert(!(await fs.pathExists(legacyDir9)), 'Claude Code setup removes legacy commands dir');
await fs.remove(tempProjectDir9); await fs.remove(tempProjectDir9);
@ -588,11 +583,6 @@ async function runTests() {
const skillFile11 = path.join(tempProjectDir11, '.agents', 'skills', 'bmad-master', 'SKILL.md'); const skillFile11 = path.join(tempProjectDir11, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile11), 'Codex install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile11), 'Codex install writes SKILL.md directory output');
// Verify name frontmatter matches directory name
const skillContent11 = await fs.readFile(skillFile11, 'utf8');
const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m);
assert(nameMatch11 && nameMatch11[1].trim() === 'bmad-master', 'Codex skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir'); assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir');
await fs.remove(tempProjectDir11); await fs.remove(tempProjectDir11);
@ -678,11 +668,6 @@ async function runTests() {
const skillFile13c = path.join(tempProjectDir13c, '.cursor', 'skills', 'bmad-master', 'SKILL.md'); const skillFile13c = path.join(tempProjectDir13c, '.cursor', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile13c), 'Cursor install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile13c), 'Cursor install writes SKILL.md directory output');
// Verify name frontmatter matches directory name
const skillContent13c = await fs.readFile(skillFile13c, 'utf8');
const nameMatch13c = skillContent13c.match(/^name:\s*(.+)$/m);
assert(nameMatch13c && nameMatch13c[1].trim() === 'bmad-master', 'Cursor skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(legacyDir13c)), 'Cursor setup removes legacy commands dir'); assert(!(await fs.pathExists(legacyDir13c)), 'Cursor setup removes legacy commands dir');
await fs.remove(tempProjectDir13c); await fs.remove(tempProjectDir13c);
@ -1301,11 +1286,6 @@ async function runTests() {
const skillFile24 = path.join(tempProjectDir24, '.iflow', 'skills', 'bmad-master', 'SKILL.md'); 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(skillFile24), 'iFlow install writes SKILL.md directory output');
// Verify name frontmatter matches directory name
const skillContent24 = await fs.readFile(skillFile24, 'utf8');
const nameMatch24 = skillContent24.match(/^name:\s*(.+)$/m);
assert(nameMatch24 && nameMatch24[1].trim() === 'bmad-master', 'iFlow skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir24, '.iflow', 'commands'))), 'iFlow setup removes legacy commands dir'); assert(!(await fs.pathExists(path.join(tempProjectDir24, '.iflow', 'commands'))), 'iFlow setup removes legacy commands dir');
await fs.remove(tempProjectDir24); await fs.remove(tempProjectDir24);
@ -1351,11 +1331,6 @@ async function runTests() {
const skillFile25 = path.join(tempProjectDir25, '.qwen', 'skills', 'bmad-master', 'SKILL.md'); 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(skillFile25), 'QwenCoder install writes SKILL.md directory output');
// Verify name frontmatter matches directory name
const skillContent25 = await fs.readFile(skillFile25, 'utf8');
const nameMatch25 = skillContent25.match(/^name:\s*(.+)$/m);
assert(nameMatch25 && nameMatch25[1].trim() === 'bmad-master', 'QwenCoder skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir25, '.qwen', 'commands'))), 'QwenCoder setup removes legacy commands dir'); assert(!(await fs.pathExists(path.join(tempProjectDir25, '.qwen', 'commands'))), 'QwenCoder setup removes legacy commands dir');
await fs.remove(tempProjectDir25); await fs.remove(tempProjectDir25);
@ -1412,11 +1387,6 @@ async function runTests() {
const skillFile26 = path.join(tempProjectDir26, '.rovodev', 'skills', 'bmad-master', 'SKILL.md'); 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(skillFile26), 'Rovo Dev install writes SKILL.md directory output');
// Verify name frontmatter matches directory name
const skillContent26 = await fs.readFile(skillFile26, 'utf8');
const nameMatch26 = skillContent26.match(/^name:\s*(.+)$/m);
assert(nameMatch26 && nameMatch26[1].trim() === 'bmad-master', 'Rovo Dev skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir26, '.rovodev', 'workflows'))), 'Rovo Dev setup removes legacy workflows dir'); 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 // Verify prompts.yml cleanup: BMAD entries removed, user entry preserved
@ -1489,9 +1459,6 @@ async function runTests() {
const newSkillFile27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-master', 'SKILL.md'); 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'); assert(await fs.pathExists(newSkillFile27), 'Fresh bmad skills are installed alongside preserved bmad-os-* skills');
// Stale non-bmad-os skill must have been removed by cleanup
assert(!(await fs.pathExists(regularSkillDir27)), 'Cleanup removes stale non-bmad-os skills');
await fs.remove(tempProjectDir27); await fs.remove(tempProjectDir27);
await fs.remove(installedBmadDir27); await fs.remove(installedBmadDir27);
} catch (error) { } catch (error) {

View File

@ -713,8 +713,7 @@ class Installer {
} }
// Merge tool selection into config (for both quick update and regular flow) // Merge tool selection into config (for both quick update and regular flow)
// Normalize IDE keys to lowercase so they match handler map keys consistently config.ides = toolSelection.ides;
config.ides = (toolSelection.ides || []).map((ide) => ide.toLowerCase());
config.skipIde = toolSelection.skipIde; config.skipIde = toolSelection.skipIde;
const ideConfigurations = toolSelection.configurations; const ideConfigurations = toolSelection.configurations;
@ -1355,19 +1354,6 @@ class Installer {
} catch { } catch {
// Ensure the original error is never swallowed by a logging failure // Ensure the original error is never swallowed by a logging failure
} }
// Clean up any temp backup directories that were created before the failure
try {
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
await fs.remove(config._tempBackupDir);
}
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
await fs.remove(config._tempModifiedBackupDir);
}
} catch {
// Best-effort cleanup — don't mask the original error
}
throw error; throw error;
} }
} }

View File

@ -793,7 +793,6 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
if (!(await fs.pathExists(filePath))) return; if (!(await fs.pathExists(filePath))) return;
try {
const content = await fs.readFile(filePath, 'utf8'); const content = await fs.readFile(filePath, 'utf8');
const startIdx = content.indexOf('<!-- BMAD:START -->'); const startIdx = content.indexOf('<!-- BMAD:START -->');
const endIdx = content.indexOf('<!-- BMAD:END -->'); const endIdx = content.indexOf('<!-- BMAD:END -->');
@ -816,9 +815,6 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
} }
if (!options.silent) await prompts.log.message(' Cleaned BMAD markers from copilot-instructions.md'); if (!options.silent) await prompts.log.message(' Cleaned BMAD markers from copilot-instructions.md');
} catch {
if (!options.silent) await prompts.log.warn(' Warning: Could not clean BMAD markers from copilot-instructions.md');
}
} }
/** /**
@ -918,9 +914,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
try { try {
if (await fs.pathExists(candidatePath)) { if (await fs.pathExists(candidatePath)) {
const entries = await fs.readdir(candidatePath); const entries = await fs.readdir(candidatePath);
const hasBmad = entries.some( const hasBmad = entries.some((e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad'));
(e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad') && !e.toLowerCase().startsWith('bmad-os-'),
);
if (hasBmad) { if (hasBmad) {
return candidatePath; return candidatePath;
} }

View File

@ -1,3 +1,5 @@
const fs = require('fs-extra');
const path = require('node:path');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
const prompts = require('../../../lib/prompts'); const prompts = require('../../../lib/prompts');
@ -6,7 +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 config-driven from platform-codes.yaml. * All platforms are now config-driven from platform-codes.yaml.
* The custom installer file mechanism is retained for future use but currently has no entries.
*/ */
class IdeManager { class IdeManager {
constructor() { constructor() {
@ -40,12 +43,50 @@ class IdeManager {
} }
/** /**
* Dynamically load all IDE handlers from platform-codes.yaml * Dynamically load all IDE handlers
* 1. Load custom installer files first (kilo.js, rovodev.js)
* 2. Load config-driven handlers from platform-codes.yaml
*/ */
async loadHandlers() { async loadHandlers() {
// Load custom installer files
await this.loadCustomInstallerFiles();
// Load config-driven handlers from platform-codes.yaml
await this.loadConfigDrivenHandlers(); await this.loadConfigDrivenHandlers();
} }
/**
* Load custom installer files (unique installation logic)
* 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)
*/
async loadCustomInstallerFiles() {
const ideDir = __dirname;
const customFiles = [];
for (const file of customFiles) {
const filePath = path.join(ideDir, file);
if (!fs.existsSync(filePath)) continue;
try {
const HandlerModule = require(filePath);
const HandlerClass = HandlerModule.default || Object.values(HandlerModule)[0];
if (HandlerClass) {
const instance = new HandlerClass();
if (instance.name && typeof instance.name === 'string') {
if (typeof instance.setBmadFolderName === 'function') {
instance.setBmadFolderName(this.bmadFolderName);
}
this.handlers.set(instance.name, instance);
}
}
} catch (error) {
await prompts.log.warn(`Warning: Could not load ${file}: ${error.message}`);
}
}
}
/** /**
* Load config-driven handlers from platform-codes.yaml * Load config-driven handlers from platform-codes.yaml
* This creates ConfigDrivenIdeSetup instances for platforms with installer config * This creates ConfigDrivenIdeSetup instances for platforms with installer config
@ -57,6 +98,9 @@ class IdeManager {
const { ConfigDrivenIdeSetup } = require('./_config-driven'); const { ConfigDrivenIdeSetup } = require('./_config-driven');
for (const [platformCode, platformInfo] of Object.entries(platformConfig.platforms)) { for (const [platformCode, platformInfo] of Object.entries(platformConfig.platforms)) {
// Skip if already loaded by custom installer
if (this.handlers.has(platformCode)) continue;
// Skip if no installer config (platform may not need installation) // Skip if no installer config (platform may not need installation)
if (!platformInfo.installer) continue; if (!platformInfo.installer) continue;
@ -145,11 +189,7 @@ class IdeManager {
} }
// Still clean up legacy artifacts so old broken configs don't linger // Still clean up legacy artifacts so old broken configs don't linger
if (typeof handler.cleanup === 'function') { if (typeof handler.cleanup === 'function') {
try {
await handler.cleanup(projectDir, { silent: true }); await handler.cleanup(projectDir, { silent: true });
} catch {
// Best-effort cleanup — don't let stale files block the suspended result
}
} }
return { success: false, ide: ideName, error: 'suspended' }; return { success: false, ide: ideName, error: 'suspended' };
} }

View File

@ -205,7 +205,7 @@ platforms:
skill_format: true skill_format: true
roo: roo:
name: "Roo Code" name: "Roo Cline"
preferred: false preferred: false
category: ide category: ide
description: "Enhanced Cline fork" description: "Enhanced Cline fork"

View File

@ -221,7 +221,7 @@ Support assumption: full Agent Skills support. BMAD currently uses a custom inst
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`. 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 @google/gemini-cli` or see [geminicli.com](https://geminicli.com) **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] 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] Implement native skills output — target_dir `.gemini/skills`, skill_format true, template_type default (replaces TOML templates)

View File

@ -50,7 +50,7 @@ platforms:
description: "AI development tool" description: "AI development tool"
roo: roo:
name: "Roo Code" name: "Roo Cline"
preferred: false preferred: false
category: ide category: ide
description: "Enhanced Cline fork" description: "Enhanced Cline fork"