diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 63f2567f5..23c8f6382 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -457,9 +457,296 @@ async function runTests() { console.log(''); // ============================================================ - // Test 9: OpenCode Ancestor Conflict + // Test 9: Claude Code Native Skills Install // ============================================================ - console.log(`${colors.yellow}Test Suite 9: OpenCode Ancestor Conflict${colors.reset}\n`); + console.log(`${colors.yellow}Test Suite 9: Claude Code Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes9 = await loadPlatformCodes(); + const claudeInstaller = platformCodes9.platforms['claude-code']?.installer; + + assert(claudeInstaller?.target_dir === '.claude/skills', 'Claude Code target_dir uses native skills path'); + + assert(claudeInstaller?.skill_format === true, 'Claude Code installer enables native skill output'); + + assert(claudeInstaller?.ancestor_conflict_check === true, 'Claude Code installer enables ancestor conflict checks'); + + assert( + Array.isArray(claudeInstaller?.legacy_targets) && claudeInstaller.legacy_targets.includes('.claude/commands'), + 'Claude Code installer cleans legacy command output', + ); + + const tempProjectDir9 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-claude-code-test-')); + const installedBmadDir9 = await createTestBmadFixture(); + const legacyDir9 = path.join(tempProjectDir9, '.claude', 'commands'); + await fs.ensureDir(legacyDir9); + await fs.writeFile(path.join(legacyDir9, 'bmad-legacy.md'), 'legacy\n'); + + const ideManager9 = new IdeManager(); + await ideManager9.ensureInitialized(); + const result9 = await ideManager9.setup('claude-code', tempProjectDir9, installedBmadDir9, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result9.success === true, 'Claude Code setup succeeds against temp project'); + + 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(legacyDir9)), 'Claude Code setup removes legacy commands dir'); + + await fs.remove(tempProjectDir9); + await fs.remove(installedBmadDir9); + } catch (error) { + assert(false, 'Claude Code native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 10: Claude Code Ancestor Conflict + // ============================================================ + console.log(`${colors.yellow}Test Suite 10: Claude Code Ancestor Conflict${colors.reset}\n`); + + try { + const tempRoot10 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-claude-code-ancestor-test-')); + const parentProjectDir10 = path.join(tempRoot10, 'parent'); + const childProjectDir10 = path.join(parentProjectDir10, 'child'); + const installedBmadDir10 = await createTestBmadFixture(); + + await fs.ensureDir(path.join(parentProjectDir10, '.git')); + await fs.ensureDir(path.join(parentProjectDir10, '.claude', 'skills', 'bmad-existing')); + await fs.ensureDir(childProjectDir10); + await fs.writeFile(path.join(parentProjectDir10, '.claude', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n'); + + const ideManager10 = new IdeManager(); + await ideManager10.ensureInitialized(); + const result10 = await ideManager10.setup('claude-code', childProjectDir10, installedBmadDir10, { + silent: true, + selectedModules: ['bmm'], + }); + const expectedConflictDir10 = await fs.realpath(path.join(parentProjectDir10, '.claude', 'skills')); + + assert(result10.success === false, 'Claude Code setup refuses install when ancestor skills already exist'); + assert(result10.handlerResult?.reason === 'ancestor-conflict', 'Claude Code ancestor rejection reports ancestor-conflict reason'); + assert( + result10.handlerResult?.conflictDir === expectedConflictDir10, + 'Claude Code ancestor rejection points at ancestor .claude/skills dir', + ); + + await fs.remove(tempRoot10); + await fs.remove(installedBmadDir10); + } catch (error) { + assert(false, 'Claude Code ancestor conflict protection test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 11: Codex Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 11: Codex Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes11 = await loadPlatformCodes(); + const codexInstaller = platformCodes11.platforms.codex?.installer; + + assert(codexInstaller?.target_dir === '.agents/skills', 'Codex target_dir uses native skills path'); + + assert(codexInstaller?.skill_format === true, 'Codex installer enables native skill output'); + + assert(codexInstaller?.ancestor_conflict_check === true, 'Codex installer enables ancestor conflict checks'); + + assert( + Array.isArray(codexInstaller?.legacy_targets) && codexInstaller.legacy_targets.includes('.codex/prompts'), + 'Codex installer cleans legacy prompt output', + ); + + const tempProjectDir11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-test-')); + const installedBmadDir11 = await createTestBmadFixture(); + const legacyDir11 = path.join(tempProjectDir11, '.codex', 'prompts'); + await fs.ensureDir(legacyDir11); + await fs.writeFile(path.join(legacyDir11, 'bmad-legacy.md'), 'legacy\n'); + + const ideManager11 = new IdeManager(); + await ideManager11.ensureInitialized(); + const result11 = await ideManager11.setup('codex', tempProjectDir11, installedBmadDir11, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result11.success === true, 'Codex setup succeeds against temp project'); + + 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(legacyDir11)), 'Codex setup removes legacy prompts dir'); + + await fs.remove(tempProjectDir11); + await fs.remove(installedBmadDir11); + } catch (error) { + assert(false, 'Codex native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 12: Codex Ancestor Conflict + // ============================================================ + console.log(`${colors.yellow}Test Suite 12: Codex Ancestor Conflict${colors.reset}\n`); + + try { + const tempRoot12 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-ancestor-test-')); + const parentProjectDir12 = path.join(tempRoot12, 'parent'); + const childProjectDir12 = path.join(parentProjectDir12, 'child'); + const installedBmadDir12 = await createTestBmadFixture(); + + await fs.ensureDir(path.join(parentProjectDir12, '.git')); + await fs.ensureDir(path.join(parentProjectDir12, '.agents', 'skills', 'bmad-existing')); + await fs.ensureDir(childProjectDir12); + await fs.writeFile(path.join(parentProjectDir12, '.agents', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n'); + + const ideManager12 = new IdeManager(); + await ideManager12.ensureInitialized(); + const result12 = await ideManager12.setup('codex', childProjectDir12, installedBmadDir12, { + silent: true, + selectedModules: ['bmm'], + }); + const expectedConflictDir12 = await fs.realpath(path.join(parentProjectDir12, '.agents', 'skills')); + + assert(result12.success === false, 'Codex setup refuses install when ancestor skills already exist'); + assert(result12.handlerResult?.reason === 'ancestor-conflict', 'Codex ancestor rejection reports ancestor-conflict reason'); + assert(result12.handlerResult?.conflictDir === expectedConflictDir12, 'Codex ancestor rejection points at ancestor .agents/skills dir'); + + await fs.remove(tempRoot12); + await fs.remove(installedBmadDir12); + } catch (error) { + assert(false, 'Codex ancestor conflict protection test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 13: Cursor Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 13: Cursor Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes13 = await loadPlatformCodes(); + const cursorInstaller = platformCodes13.platforms.cursor?.installer; + + assert(cursorInstaller?.target_dir === '.cursor/skills', 'Cursor target_dir uses native skills path'); + + assert(cursorInstaller?.skill_format === true, 'Cursor installer enables native skill output'); + + assert( + Array.isArray(cursorInstaller?.legacy_targets) && cursorInstaller.legacy_targets.includes('.cursor/commands'), + 'Cursor installer cleans legacy command output', + ); + + assert(!cursorInstaller?.ancestor_conflict_check, 'Cursor installer does not enable ancestor conflict checks'); + + const tempProjectDir13c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cursor-test-')); + const installedBmadDir13c = await createTestBmadFixture(); + const legacyDir13c = path.join(tempProjectDir13c, '.cursor', 'commands'); + await fs.ensureDir(legacyDir13c); + await fs.writeFile(path.join(legacyDir13c, 'bmad-legacy.md'), 'legacy\n'); + + const ideManager13c = new IdeManager(); + await ideManager13c.ensureInitialized(); + const result13c = await ideManager13c.setup('cursor', tempProjectDir13c, installedBmadDir13c, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result13c.success === true, 'Cursor setup succeeds against temp project'); + + 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(legacyDir13c)), 'Cursor setup removes legacy commands dir'); + + await fs.remove(tempProjectDir13c); + await fs.remove(installedBmadDir13c); + } catch (error) { + assert(false, 'Cursor native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 14: Roo Code Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 14: Roo Code Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes13 = await loadPlatformCodes(); + const rooInstaller = platformCodes13.platforms.roo?.installer; + + assert(rooInstaller?.target_dir === '.roo/skills', 'Roo target_dir uses native skills path'); + + assert(rooInstaller?.skill_format === true, 'Roo installer enables native skill output'); + + assert( + Array.isArray(rooInstaller?.legacy_targets) && rooInstaller.legacy_targets.includes('.roo/commands'), + 'Roo installer cleans legacy command output', + ); + + const tempProjectDir13 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-roo-test-')); + const installedBmadDir13 = await createTestBmadFixture(); + const legacyDir13 = path.join(tempProjectDir13, '.roo', 'commands', 'bmad-legacy-dir'); + await fs.ensureDir(legacyDir13); + await fs.writeFile(path.join(tempProjectDir13, '.roo', 'commands', 'bmad-legacy.md'), 'legacy\n'); + await fs.writeFile(path.join(legacyDir13, 'SKILL.md'), 'legacy\n'); + + const ideManager13 = new IdeManager(); + await ideManager13.ensureInitialized(); + const result13 = await ideManager13.setup('roo', tempProjectDir13, installedBmadDir13, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result13.success === true, 'Roo setup succeeds against temp project'); + + const skillFile13 = path.join(tempProjectDir13, '.roo', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile13), 'Roo install writes SKILL.md directory output'); + + // Verify name frontmatter matches directory name (Roo constraint: lowercase alphanumeric + hyphens) + const skillContent13 = await fs.readFile(skillFile13, 'utf8'); + const nameMatch13 = skillContent13.match(/^name:\s*(.+)$/m); + assert( + nameMatch13 && nameMatch13[1].trim() === 'bmad-master', + 'Roo skill name frontmatter matches directory name exactly (lowercase alphanumeric + hyphens)', + ); + + assert(!(await fs.pathExists(path.join(tempProjectDir13, '.roo', 'commands'))), 'Roo setup removes legacy commands dir'); + + // Reinstall/upgrade: run setup again over existing skills output + const result13b = await ideManager13.setup('roo', tempProjectDir13, installedBmadDir13, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result13b.success === true, 'Roo reinstall/upgrade succeeds over existing skills'); + assert(await fs.pathExists(skillFile13), 'Roo reinstall preserves SKILL.md output'); + + await fs.remove(tempProjectDir13); + await fs.remove(installedBmadDir13); + } catch (error) { + assert(false, 'Roo native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 15: OpenCode Ancestor Conflict + // ============================================================ + console.log(`${colors.yellow}Test Suite 15: OpenCode Ancestor Conflict${colors.reset}\n`); try { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-ancestor-test-')); @@ -496,9 +783,9 @@ async function runTests() { console.log(''); // ============================================================ - // Test 10: QA Agent Compilation + // Test 16: QA Agent Compilation // ============================================================ - console.log(`${colors.yellow}Test Suite 10: QA Agent Compilation${colors.reset}\n`); + console.log(`${colors.yellow}Test Suite 16: QA Agent Compilation${colors.reset}\n`); try { const builder = new YamlXmlBuilder(); @@ -524,6 +811,410 @@ async function runTests() { console.log(''); + // ============================================================ + // Test 17: GitHub Copilot Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 17: GitHub Copilot Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes17 = await loadPlatformCodes(); + const copilotInstaller = platformCodes17.platforms['github-copilot']?.installer; + + assert(copilotInstaller?.target_dir === '.github/skills', 'GitHub Copilot target_dir uses native skills path'); + + assert(copilotInstaller?.skill_format === true, 'GitHub Copilot installer enables native skill output'); + + assert( + Array.isArray(copilotInstaller?.legacy_targets) && copilotInstaller.legacy_targets.includes('.github/agents'), + 'GitHub Copilot installer cleans legacy agents output', + ); + + assert( + Array.isArray(copilotInstaller?.legacy_targets) && copilotInstaller.legacy_targets.includes('.github/prompts'), + 'GitHub Copilot installer cleans legacy prompts output', + ); + + const tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-')); + const installedBmadDir17 = await createTestBmadFixture(); + + // Create legacy .github/agents/ and .github/prompts/ files + const legacyAgentsDir17 = path.join(tempProjectDir17, '.github', 'agents'); + const legacyPromptsDir17 = path.join(tempProjectDir17, '.github', 'prompts'); + await fs.ensureDir(legacyAgentsDir17); + await fs.ensureDir(legacyPromptsDir17); + await fs.writeFile(path.join(legacyAgentsDir17, 'bmad-legacy.agent.md'), 'legacy agent\n'); + await fs.writeFile(path.join(legacyPromptsDir17, 'bmad-legacy.prompt.md'), 'legacy prompt\n'); + + // Create legacy copilot-instructions.md with BMAD markers + const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md'); + await fs.writeFile( + copilotInstructionsPath17, + 'User content before\n\nBMAD generated content\n\nUser content after\n', + ); + + const ideManager17 = new IdeManager(); + await ideManager17.ensureInitialized(); + const result17 = await ideManager17.setup('github-copilot', tempProjectDir17, installedBmadDir17, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result17.success === true, 'GitHub Copilot setup succeeds against temp project'); + + const skillFile17 = path.join(tempProjectDir17, '.github', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile17), 'GitHub Copilot install writes SKILL.md directory output'); + + // Verify name frontmatter matches directory name + const skillContent17 = await fs.readFile(skillFile17, 'utf8'); + const nameMatch17 = skillContent17.match(/^name:\s*(.+)$/m); + assert(nameMatch17 && nameMatch17[1].trim() === 'bmad-master', 'GitHub Copilot skill name frontmatter matches directory name exactly'); + + assert(!(await fs.pathExists(legacyAgentsDir17)), 'GitHub Copilot setup removes legacy agents dir'); + + assert(!(await fs.pathExists(legacyPromptsDir17)), 'GitHub Copilot setup removes legacy prompts dir'); + + // Verify copilot-instructions.md BMAD markers were stripped but user content preserved + const cleanedInstructions17 = await fs.readFile(copilotInstructionsPath17, 'utf8'); + assert( + !cleanedInstructions17.includes('BMAD:START') && !cleanedInstructions17.includes('BMAD generated content'), + 'GitHub Copilot setup strips BMAD markers from copilot-instructions.md', + ); + assert( + cleanedInstructions17.includes('User content before') && cleanedInstructions17.includes('User content after'), + 'GitHub Copilot setup preserves user content in copilot-instructions.md', + ); + + await fs.remove(tempProjectDir17); + await fs.remove(installedBmadDir17); + } catch (error) { + assert(false, 'GitHub Copilot native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 18: Cline Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 18: Cline Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes18 = await loadPlatformCodes(); + const clineInstaller = platformCodes18.platforms.cline?.installer; + + assert(clineInstaller?.target_dir === '.cline/skills', 'Cline target_dir uses native skills path'); + + assert(clineInstaller?.skill_format === true, 'Cline installer enables native skill output'); + + assert( + Array.isArray(clineInstaller?.legacy_targets) && clineInstaller.legacy_targets.includes('.clinerules/workflows'), + 'Cline installer cleans legacy workflow output', + ); + + const tempProjectDir18 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cline-test-')); + const installedBmadDir18 = await createTestBmadFixture(); + const legacyDir18 = path.join(tempProjectDir18, '.clinerules', 'workflows', 'bmad-legacy-dir'); + await fs.ensureDir(legacyDir18); + await fs.writeFile(path.join(tempProjectDir18, '.clinerules', 'workflows', 'bmad-legacy.md'), 'legacy\n'); + await fs.writeFile(path.join(legacyDir18, 'SKILL.md'), 'legacy\n'); + + const ideManager18 = new IdeManager(); + await ideManager18.ensureInitialized(); + const result18 = await ideManager18.setup('cline', tempProjectDir18, installedBmadDir18, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result18.success === true, 'Cline setup succeeds against temp project'); + + const skillFile18 = path.join(tempProjectDir18, '.cline', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile18), 'Cline install writes SKILL.md directory output'); + + // Verify name frontmatter matches directory name + const skillContent18 = await fs.readFile(skillFile18, 'utf8'); + const nameMatch18 = skillContent18.match(/^name:\s*(.+)$/m); + assert(nameMatch18 && nameMatch18[1].trim() === 'bmad-master', 'Cline skill name frontmatter matches directory name exactly'); + + assert(!(await fs.pathExists(path.join(tempProjectDir18, '.clinerules', 'workflows'))), 'Cline setup removes legacy workflows dir'); + + // Reinstall/upgrade: run setup again over existing skills output + const result18b = await ideManager18.setup('cline', tempProjectDir18, installedBmadDir18, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result18b.success === true, 'Cline reinstall/upgrade succeeds over existing skills'); + assert(await fs.pathExists(skillFile18), 'Cline reinstall preserves SKILL.md output'); + + await fs.remove(tempProjectDir18); + await fs.remove(installedBmadDir18); + } catch (error) { + assert(false, 'Cline native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 19: CodeBuddy Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 19: CodeBuddy Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes19 = await loadPlatformCodes(); + const codebuddyInstaller = platformCodes19.platforms.codebuddy?.installer; + + assert(codebuddyInstaller?.target_dir === '.codebuddy/skills', 'CodeBuddy target_dir uses native skills path'); + + assert(codebuddyInstaller?.skill_format === true, 'CodeBuddy installer enables native skill output'); + + assert( + Array.isArray(codebuddyInstaller?.legacy_targets) && codebuddyInstaller.legacy_targets.includes('.codebuddy/commands'), + 'CodeBuddy installer cleans legacy command output', + ); + + const tempProjectDir19 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codebuddy-test-')); + const installedBmadDir19 = await createTestBmadFixture(); + const legacyDir19 = path.join(tempProjectDir19, '.codebuddy', 'commands', 'bmad-legacy-dir'); + await fs.ensureDir(legacyDir19); + await fs.writeFile(path.join(tempProjectDir19, '.codebuddy', 'commands', 'bmad-legacy.md'), 'legacy\n'); + await fs.writeFile(path.join(legacyDir19, 'SKILL.md'), 'legacy\n'); + + const ideManager19 = new IdeManager(); + await ideManager19.ensureInitialized(); + const result19 = await ideManager19.setup('codebuddy', tempProjectDir19, installedBmadDir19, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result19.success === true, 'CodeBuddy setup succeeds against temp project'); + + const skillFile19 = path.join(tempProjectDir19, '.codebuddy', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile19), 'CodeBuddy install writes SKILL.md directory output'); + + const skillContent19 = await fs.readFile(skillFile19, 'utf8'); + const nameMatch19 = skillContent19.match(/^name:\s*(.+)$/m); + assert(nameMatch19 && nameMatch19[1].trim() === 'bmad-master', 'CodeBuddy skill name frontmatter matches directory name exactly'); + + assert(!(await fs.pathExists(path.join(tempProjectDir19, '.codebuddy', 'commands'))), 'CodeBuddy setup removes legacy commands dir'); + + const result19b = await ideManager19.setup('codebuddy', tempProjectDir19, installedBmadDir19, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result19b.success === true, 'CodeBuddy reinstall/upgrade succeeds over existing skills'); + assert(await fs.pathExists(skillFile19), 'CodeBuddy reinstall preserves SKILL.md output'); + + await fs.remove(tempProjectDir19); + await fs.remove(installedBmadDir19); + } catch (error) { + assert(false, 'CodeBuddy native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 20: Crush Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 20: Crush Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes20 = await loadPlatformCodes(); + const crushInstaller = platformCodes20.platforms.crush?.installer; + + assert(crushInstaller?.target_dir === '.crush/skills', 'Crush target_dir uses native skills path'); + + assert(crushInstaller?.skill_format === true, 'Crush installer enables native skill output'); + + assert( + Array.isArray(crushInstaller?.legacy_targets) && crushInstaller.legacy_targets.includes('.crush/commands'), + 'Crush installer cleans legacy command output', + ); + + const tempProjectDir20 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-crush-test-')); + const installedBmadDir20 = await createTestBmadFixture(); + const legacyDir20 = path.join(tempProjectDir20, '.crush', 'commands', 'bmad-legacy-dir'); + await fs.ensureDir(legacyDir20); + await fs.writeFile(path.join(tempProjectDir20, '.crush', 'commands', 'bmad-legacy.md'), 'legacy\n'); + await fs.writeFile(path.join(legacyDir20, 'SKILL.md'), 'legacy\n'); + + const ideManager20 = new IdeManager(); + await ideManager20.ensureInitialized(); + const result20 = await ideManager20.setup('crush', tempProjectDir20, installedBmadDir20, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result20.success === true, 'Crush setup succeeds against temp project'); + + const skillFile20 = path.join(tempProjectDir20, '.crush', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile20), 'Crush install writes SKILL.md directory output'); + + const skillContent20 = await fs.readFile(skillFile20, 'utf8'); + const nameMatch20 = skillContent20.match(/^name:\s*(.+)$/m); + assert(nameMatch20 && nameMatch20[1].trim() === 'bmad-master', 'Crush skill name frontmatter matches directory name exactly'); + + assert(!(await fs.pathExists(path.join(tempProjectDir20, '.crush', 'commands'))), 'Crush setup removes legacy commands dir'); + + const result20b = await ideManager20.setup('crush', tempProjectDir20, installedBmadDir20, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result20b.success === true, 'Crush reinstall/upgrade succeeds over existing skills'); + assert(await fs.pathExists(skillFile20), 'Crush reinstall preserves SKILL.md output'); + + await fs.remove(tempProjectDir20); + await fs.remove(installedBmadDir20); + } catch (error) { + assert(false, 'Crush native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 21: Trae Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 21: Trae Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes21 = await loadPlatformCodes(); + const traeInstaller = platformCodes21.platforms.trae?.installer; + + assert(traeInstaller?.target_dir === '.trae/skills', 'Trae target_dir uses native skills path'); + + assert(traeInstaller?.skill_format === true, 'Trae installer enables native skill output'); + + assert( + Array.isArray(traeInstaller?.legacy_targets) && traeInstaller.legacy_targets.includes('.trae/rules'), + 'Trae installer cleans legacy rules output', + ); + + const tempProjectDir21 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-trae-test-')); + const installedBmadDir21 = await createTestBmadFixture(); + const legacyDir21 = path.join(tempProjectDir21, '.trae', 'rules'); + await fs.ensureDir(legacyDir21); + await fs.writeFile(path.join(legacyDir21, 'bmad-legacy.md'), 'legacy\n'); + + const ideManager21 = new IdeManager(); + await ideManager21.ensureInitialized(); + const result21 = await ideManager21.setup('trae', tempProjectDir21, installedBmadDir21, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result21.success === true, 'Trae setup succeeds against temp project'); + + const skillFile21 = path.join(tempProjectDir21, '.trae', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile21), 'Trae install writes SKILL.md directory output'); + + const skillContent21 = await fs.readFile(skillFile21, 'utf8'); + const nameMatch21 = skillContent21.match(/^name:\s*(.+)$/m); + assert(nameMatch21 && nameMatch21[1].trim() === 'bmad-master', 'Trae skill name frontmatter matches directory name exactly'); + + assert(!(await fs.pathExists(path.join(tempProjectDir21, '.trae', 'rules'))), 'Trae setup removes legacy rules dir'); + + const result21b = await ideManager21.setup('trae', tempProjectDir21, installedBmadDir21, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result21b.success === true, 'Trae reinstall/upgrade succeeds over existing skills'); + assert(await fs.pathExists(skillFile21), 'Trae reinstall preserves SKILL.md output'); + + await fs.remove(tempProjectDir21); + await fs.remove(installedBmadDir21); + } catch (error) { + assert(false, 'Trae native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Suite 22: KiloCoder Native Skills + // ============================================================ + console.log(`${colors.yellow}Test Suite 22: KiloCoder Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes22 = await loadPlatformCodes(); + const kiloInstaller = platformCodes22.platforms.kilo?.installer; + + assert(kiloInstaller?.target_dir === '.kilocode/skills', 'KiloCoder target_dir uses native skills path'); + + assert(kiloInstaller?.skill_format === true, 'KiloCoder installer enables native skill output'); + + assert( + Array.isArray(kiloInstaller?.legacy_targets) && kiloInstaller.legacy_targets.includes('.kilocode/workflows'), + 'KiloCoder installer cleans legacy workflows output', + ); + + // Fresh install test + const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-')); + const installedBmadDir22 = await createTestBmadFixture(); + const legacyDir22 = path.join(tempProjectDir22, '.kilocode', 'workflows'); + await fs.ensureDir(legacyDir22); + 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, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result22.success === true, 'KiloCoder setup succeeds against temp project'); + + const skillFile22 = path.join(tempProjectDir22, '.kilocode', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile22), 'KiloCoder install writes SKILL.md directory output'); + + 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( + Array.isArray(cleanedModes22.customModes) && cleanedModes22.customModes.length === 1, + '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(installedBmadDir22); + } catch (error) { + assert(false, 'KiloCoder native skills migration test succeeds', error.message); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index d23d8d6d0..3ade16b47 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -655,6 +655,16 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} } } + // Strip BMAD markers from copilot-instructions.md if present + if (this.name === 'github-copilot') { + await this.cleanupCopilotInstructions(projectDir, options); + } + + // Strip BMAD modes from .kilocodemodes if present + if (this.name === 'kilo') { + await this.cleanupKiloModes(projectDir, options); + } + // Clean all target directories if (this.installerConfig?.targets) { const parentDirs = new Set(); @@ -768,6 +778,76 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} } } } + /** + * Strip BMAD-owned content from .github/copilot-instructions.md. + * The old custom installer injected content between and markers. + * Deletes the file if nothing remains. Restores .bak backup if one exists. + */ + async cleanupCopilotInstructions(projectDir, options = {}) { + const filePath = path.join(projectDir, '.github', 'copilot-instructions.md'); + + if (!(await fs.pathExists(filePath))) return; + + const content = await fs.readFile(filePath, 'utf8'); + const startIdx = content.indexOf(''); + const endIdx = content.indexOf(''); + + if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return; + + const cleaned = content.slice(0, startIdx) + content.slice(endIdx + ''.length); + + if (cleaned.trim().length === 0) { + await fs.remove(filePath); + const backupPath = `${filePath}.bak`; + if (await fs.pathExists(backupPath)) { + await fs.rename(backupPath, filePath); + if (!options.silent) await prompts.log.message(' Restored copilot-instructions.md from backup'); + } + } else { + await fs.writeFile(filePath, cleaned, 'utf8'); + const backupPath = `${filePath}.bak`; + if (await fs.pathExists(backupPath)) await fs.remove(backupPath); + } + + if (!options.silent) await prompts.log.message(' Cleaned BMAD markers from copilot-instructions.md'); + } + + /** + * Strip BMAD-owned modes from .kilocodemodes. + * The old custom kilo.js installer added modes with slug starting with 'bmad-'. + * Parses YAML, filters out BMAD modes, rewrites. Leaves file as-is on parse failure. + */ + async cleanupKiloModes(projectDir, options = {}) { + const kiloModesPath = path.join(projectDir, '.kilocodemodes'); + + if (!(await fs.pathExists(kiloModesPath))) return; + + const content = await fs.readFile(kiloModesPath, 'utf8'); + + let config; + try { + config = yaml.parse(content) || {}; + } catch { + if (!options.silent) await prompts.log.warn(' Warning: Could not parse .kilocodemodes for cleanup'); + return; + } + + if (!Array.isArray(config.customModes)) return; + + const originalCount = config.customModes.length; + config.customModes = config.customModes.filter((mode) => mode && (!mode.slug || !mode.slug.startsWith('bmad-'))); + const removedCount = originalCount - config.customModes.length; + + if (removedCount > 0) { + try { + await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 })); + if (!options.silent) await prompts.log.message(` Removed ${removedCount} BMAD modes from .kilocodemodes`); + } catch { + if (!options.silent) await prompts.log.warn(' Warning: Could not write .kilocodemodes during cleanup'); + } + } + } + /** * Check ancestor directories for existing BMAD files in the same target_dir. * IDEs like Claude Code inherit commands from parent directories, so an existing diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js deleted file mode 100644 index 059127f81..000000000 --- a/tools/cli/installers/lib/ide/github-copilot.js +++ /dev/null @@ -1,699 +0,0 @@ -const path = require('node:path'); -const { BaseIdeSetup } = require('./_base-ide'); -const prompts = require('../../../lib/prompts'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { BMAD_FOLDER_NAME, toDashPath } = require('./shared/path-utils'); -const fs = require('fs-extra'); -const csv = require('csv-parse/sync'); -const yaml = require('yaml'); - -/** - * GitHub Copilot setup handler - * Creates agents in .github/agents/, prompts in .github/prompts/, - * copilot-instructions.md, and configures VS Code settings - */ -class GitHubCopilotSetup extends BaseIdeSetup { - constructor() { - super('github-copilot', 'GitHub Copilot', false); - // Don't set configDir to '.github' — nearly every GitHub repo has that directory, - // which would cause the base detect() to false-positive. Use detectionPaths instead. - this.configDir = null; - this.githubDir = '.github'; - this.agentsDir = 'agents'; - this.promptsDir = 'prompts'; - this.detectionPaths = ['.github/copilot-instructions.md', '.github/agents']; - } - - /** - * Setup GitHub Copilot configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); - - // Create .github/agents and .github/prompts directories - const githubDir = path.join(projectDir, this.githubDir); - const agentsDir = path.join(githubDir, this.agentsDir); - const promptsDir = path.join(githubDir, this.promptsDir); - await this.ensureDir(agentsDir); - await this.ensureDir(promptsDir); - - // Preserve any customised tool permissions from existing files before cleanup - this.existingToolPermissions = await this.collectExistingToolPermissions(projectDir); - - // Clean up any existing BMAD files before reinstalling - await this.cleanup(projectDir); - - // Load agent manifest for enriched descriptions - const agentManifest = await this.loadAgentManifest(bmadDir); - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Create agent .agent.md files - let agentCount = 0; - for (const artifact of agentArtifacts) { - const agentMeta = agentManifest.get(artifact.name); - - // Compute fileName first so we can look up any existing tool permissions - const dashName = toDashPath(artifact.relativePath); - const fileName = dashName.replace(/\.md$/, '.agent.md'); - const toolsStr = this.getToolsForFile(fileName); - const agentContent = this.createAgentContent(artifact, agentMeta, toolsStr); - const targetPath = path.join(agentsDir, fileName); - await this.writeFile(targetPath, agentContent); - agentCount++; - } - - // Generate prompt files from bmad-help.csv - const promptCount = await this.generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest); - - // Generate copilot-instructions.md - await this.generateCopilotInstructions(projectDir, bmadDir, agentManifest, options); - - if (!options.silent) await prompts.log.success(`${this.name} configured: ${agentCount} agents, ${promptCount} prompts → .github/`); - - return { - success: true, - results: { - agents: agentCount, - workflows: promptCount, - tasks: 0, - tools: 0, - }, - }; - } - - /** - * Load agent manifest CSV into a Map keyed by agent name - * @param {string} bmadDir - BMAD installation directory - * @returns {Map} Agent metadata keyed by name - */ - async loadAgentManifest(bmadDir) { - const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv'); - const agents = new Map(); - - if (!(await fs.pathExists(manifestPath))) { - return agents; - } - - try { - const csvContent = await fs.readFile(manifestPath, 'utf8'); - const records = csv.parse(csvContent, { - columns: true, - skip_empty_lines: true, - }); - - for (const record of records) { - agents.set(record.name, record); - } - } catch { - // Gracefully degrade if manifest is unreadable/malformed - } - - return agents; - } - - /** - * Load bmad-help.csv to drive prompt generation - * @param {string} bmadDir - BMAD installation directory - * @returns {Array|null} Parsed CSV rows - */ - async loadBmadHelp(bmadDir) { - const helpPath = path.join(bmadDir, '_config', 'bmad-help.csv'); - - if (!(await fs.pathExists(helpPath))) { - return null; - } - - try { - const csvContent = await fs.readFile(helpPath, 'utf8'); - return csv.parse(csvContent, { - columns: true, - skip_empty_lines: true, - }); - } catch { - // Gracefully degrade if help CSV is unreadable/malformed - return null; - } - } - - /** - * Create agent .agent.md content with enriched description - * @param {Object} artifact - Agent artifact from AgentCommandGenerator - * @param {Object|undefined} manifestEntry - Agent manifest entry with metadata - * @returns {string} Agent file content - */ - createAgentContent(artifact, manifestEntry, toolsStr) { - // Build enriched description from manifest metadata - let description; - if (manifestEntry) { - const persona = manifestEntry.displayName || artifact.name; - const title = manifestEntry.title || this.formatTitle(artifact.name); - const capabilities = manifestEntry.capabilities || 'agent capabilities'; - description = `${persona} — ${title}: ${capabilities}`; - } else { - description = `Activates the ${this.formatTitle(artifact.name)} agent persona.`; - } - - // Build the agent file path for the activation block - const agentPath = artifact.agentPath || artifact.relativePath; - const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`; - - return `--- -description: '${description.replaceAll("'", "''")}' -tools: ${toolsStr} ---- - -You must fully embody this agent's persona and follow all activation instructions exactly as specified. - - -1. LOAD the FULL agent file from ${agentFilePath} -2. READ its entire contents - this contains the complete agent persona, menu, and instructions -3. FOLLOW every step in the section precisely -4. DISPLAY the welcome/greeting as instructed -5. PRESENT the numbered menu -6. WAIT for user input before proceeding - -`; - } - - /** - * Generate .prompt.md files for workflows, tasks, tech-writer commands, and agent activators - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Array} agentArtifacts - Agent artifacts for activator generation - * @param {Map} agentManifest - Agent manifest data - * @returns {number} Count of prompts generated - */ - async generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest) { - const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir); - let promptCount = 0; - - // Load bmad-help.csv to drive workflow/task prompt generation - const helpEntries = await this.loadBmadHelp(bmadDir); - - if (helpEntries) { - for (const entry of helpEntries) { - const command = entry.command; - if (!command) continue; // Skip entries without a command (tech-writer commands have no command column) - - const workflowFile = entry['workflow-file']; - if (!workflowFile) continue; // Skip entries with no workflow file path - const promptFileName = `${command}.prompt.md`; - const toolsStr = this.getToolsForFile(promptFileName); - const promptContent = this.createWorkflowPromptContent(entry, workflowFile, toolsStr); - const promptPath = path.join(promptsDir, promptFileName); - await this.writeFile(promptPath, promptContent); - promptCount++; - } - - // Generate tech-writer command prompts (entries with no command column) - for (const entry of helpEntries) { - if (entry.command) continue; // Already handled above - const techWriterPrompt = this.createTechWriterPromptContent(entry); - if (techWriterPrompt) { - const promptFileName = `${techWriterPrompt.fileName}.prompt.md`; - const promptPath = path.join(promptsDir, promptFileName); - await this.writeFile(promptPath, techWriterPrompt.content); - promptCount++; - } - } - } - - // Generate agent activator prompts (Pattern D) - for (const artifact of agentArtifacts) { - const agentMeta = agentManifest.get(artifact.name); - const fileName = `bmad-${artifact.name}.prompt.md`; - const toolsStr = this.getToolsForFile(fileName); - const promptContent = this.createAgentActivatorPromptContent(artifact, agentMeta, toolsStr); - const promptPath = path.join(promptsDir, fileName); - await this.writeFile(promptPath, promptContent); - promptCount++; - } - - return promptCount; - } - - /** - * Create prompt content for a workflow/task entry from bmad-help.csv - * Determines the pattern (A, B, or A for .xml tasks) based on file extension - * @param {Object} entry - bmad-help.csv row - * @param {string} workflowFile - Workflow file path - * @returns {string} Prompt file content - */ - createWorkflowPromptContent(entry, workflowFile, toolsStr) { - const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name)); - // bmm/config.yaml is safe to hardcode here: these prompts are only generated when - // bmad-help.csv exists (bmm module data), so bmm is guaranteed to be installed. - const configLine = `1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables`; - - let body; - if (workflowFile.endsWith('.yaml')) { - // Pattern B: YAML-based workflows — use workflow engine - body = `${configLine} -2. Load the workflow engine at {project-root}/${this.bmadFolderName}/core/tasks/workflow.xml -3. Load and execute the workflow configuration at {project-root}/${workflowFile} using the engine from step 2`; - } else if (workflowFile.endsWith('.xml')) { - // Pattern A variant: XML tasks — load and execute directly - body = `${configLine} -2. Load and execute the task at {project-root}/${workflowFile}`; - } else { - // Pattern A: MD workflows — load and follow directly - body = `${configLine} -2. Load and follow the workflow at {project-root}/${workflowFile}`; - } - - return `--- -description: '${description}' -agent: 'agent' -tools: ${toolsStr} ---- - -${body} -`; - } - - /** - * Create a short 2-5 word description for a prompt from the entry name - * @param {string} name - Entry name from bmad-help.csv - * @returns {string} Short description - */ - createPromptDescription(name) { - const descriptionMap = { - 'Brainstorm Project': 'Brainstorm ideas', - 'Market Research': 'Market research', - 'Domain Research': 'Domain research', - 'Technical Research': 'Technical research', - 'Create Brief': 'Create product brief', - 'Create PRD': 'Create PRD', - 'Validate PRD': 'Validate PRD', - 'Edit PRD': 'Edit PRD', - 'Create UX': 'Create UX design', - 'Create Architecture': 'Create architecture', - 'Create Epics and Stories': 'Create epics and stories', - 'Check Implementation Readiness': 'Check implementation readiness', - 'Sprint Planning': 'Sprint planning', - 'Sprint Status': 'Sprint status', - 'Create Story': 'Create story', - 'Validate Story': 'Validate story', - 'Dev Story': 'Dev story', - 'QA Automation Test': 'QA automation', - 'Code Review': 'Code review', - Retrospective: 'Retrospective', - 'Document Project': 'Document project', - 'Generate Project Context': 'Generate project context', - 'Quick Spec': 'Quick spec', - 'Quick Dev': 'Quick dev', - 'Correct Course': 'Correct course', - Brainstorming: 'Brainstorm ideas', - 'Party Mode': 'Party mode', - 'bmad-help': 'BMAD help', - 'Index Docs': 'Index documents', - 'Shard Document': 'Shard document', - 'Editorial Review - Prose': 'Editorial review prose', - 'Editorial Review - Structure': 'Editorial review structure', - 'Adversarial Review (General)': 'Adversarial review', - }; - - return descriptionMap[name] || name; - } - - /** - * Create prompt content for tech-writer agent-only commands (Pattern C) - * @param {Object} entry - bmad-help.csv row - * @returns {Object|null} { fileName, content } or null if not a tech-writer command - */ - createTechWriterPromptContent(entry) { - if (entry['agent-name'] !== 'tech-writer') return null; - - const techWriterCommands = { - 'Write Document': { code: 'WD', file: 'bmad-bmm-write-document', description: 'Write document' }, - 'Update Standards': { code: 'US', file: 'bmad-bmm-update-standards', description: 'Update standards' }, - 'Mermaid Generate': { code: 'MG', file: 'bmad-bmm-mermaid-generate', description: 'Mermaid generate' }, - 'Validate Document': { code: 'VD', file: 'bmad-bmm-validate-document', description: 'Validate document' }, - 'Explain Concept': { code: 'EC', file: 'bmad-bmm-explain-concept', description: 'Explain concept' }, - }; - - const cmd = techWriterCommands[entry.name]; - if (!cmd) return null; - - const safeDescription = this.escapeYamlSingleQuote(cmd.description); - const toolsStr = this.getToolsForFile(`${cmd.file}.prompt.md`); - - const content = `--- -description: '${safeDescription}' -agent: 'agent' -tools: ${toolsStr} ---- - -1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables -2. Load the full agent file from {project-root}/${this.bmadFolderName}/bmm/agents/tech-writer/tech-writer.md and activate the Paige (Technical Writer) persona -3. Execute the ${entry.name} menu command (${cmd.code}) -`; - - return { fileName: cmd.file, content }; - } - - /** - * Create agent activator prompt content (Pattern D) - * @param {Object} artifact - Agent artifact - * @param {Object|undefined} manifestEntry - Agent manifest entry - * @returns {string} Prompt file content - */ - createAgentActivatorPromptContent(artifact, manifestEntry, toolsStr) { - let description; - if (manifestEntry) { - description = manifestEntry.title || this.formatTitle(artifact.name); - } else { - description = this.formatTitle(artifact.name); - } - - const safeDescription = this.escapeYamlSingleQuote(description); - const agentPath = artifact.agentPath || artifact.relativePath; - const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`; - - // bmm/config.yaml is safe to hardcode: agent activators are only generated from - // bmm agent artifacts, so bmm is guaranteed to be installed. - return `--- -description: '${safeDescription}' -agent: 'agent' -tools: ${toolsStr} ---- - -1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables -2. Load the full agent file from ${agentFilePath} -3. Follow ALL activation instructions in the agent file -4. Display the welcome/greeting as instructed -5. Present the numbered menu -6. Wait for user input before proceeding -`; - } - - /** - * Generate copilot-instructions.md from module config - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Map} agentManifest - Agent manifest data - */ - async generateCopilotInstructions(projectDir, bmadDir, agentManifest, options = {}) { - const configVars = await this.loadModuleConfig(bmadDir); - - // Build the agents table from the manifest - let agentsTable = '| Agent | Persona | Title | Capabilities |\n|---|---|---|---|\n'; - const agentOrder = [ - 'bmad-master', - 'analyst', - 'architect', - 'dev', - 'pm', - 'qa', - 'quick-flow-solo-dev', - 'sm', - 'tech-writer', - 'ux-designer', - ]; - - for (const agentName of agentOrder) { - const meta = agentManifest.get(agentName); - if (meta) { - const capabilities = meta.capabilities || 'agent capabilities'; - const cleanTitle = (meta.title || '').replaceAll('""', '"'); - agentsTable += `| ${agentName} | ${meta.displayName} | ${cleanTitle} | ${capabilities} |\n`; - } - } - - const bmad = this.bmadFolderName; - const bmadSection = `# BMAD Method — Project Instructions - -## Project Configuration - -- **Project**: ${configVars.project_name || '{{project_name}}'} -- **User**: ${configVars.user_name || '{{user_name}}'} -- **Communication Language**: ${configVars.communication_language || '{{communication_language}}'} -- **Document Output Language**: ${configVars.document_output_language || '{{document_output_language}}'} -- **User Skill Level**: ${configVars.user_skill_level || '{{user_skill_level}}'} -- **Output Folder**: ${configVars.output_folder || '{{output_folder}}'} -- **Planning Artifacts**: ${configVars.planning_artifacts || '{{planning_artifacts}}'} -- **Implementation Artifacts**: ${configVars.implementation_artifacts || '{{implementation_artifacts}}'} -- **Project Knowledge**: ${configVars.project_knowledge || '{{project_knowledge}}'} - -## BMAD Runtime Structure - -- **Agent definitions**: \`${bmad}/bmm/agents/\` (BMM module) and \`${bmad}/core/agents/\` (core) -- **Workflow definitions**: \`${bmad}/bmm/workflows/\` (organized by phase) -- **Core tasks**: \`${bmad}/core/tasks/\` (help, editorial review, indexing, sharding, adversarial review) -- **Core workflows**: \`${bmad}/core/workflows/\` (brainstorming, party-mode, advanced-elicitation) -- **Workflow engine**: \`${bmad}/core/tasks/workflow.xml\` (executes YAML-based workflows) -- **Module configuration**: \`${bmad}/bmm/config.yaml\` -- **Core configuration**: \`${bmad}/core/config.yaml\` -- **Agent manifest**: \`${bmad}/_config/agent-manifest.csv\` -- **Workflow manifest**: \`${bmad}/_config/workflow-manifest.csv\` -- **Help manifest**: \`${bmad}/_config/bmad-help.csv\` -- **Agent memory**: \`${bmad}/_memory/\` - -## Key Conventions - -- Always load \`${bmad}/bmm/config.yaml\` before any agent activation or workflow execution -- Store all config fields as session variables: \`{user_name}\`, \`{communication_language}\`, \`{output_folder}\`, \`{planning_artifacts}\`, \`{implementation_artifacts}\`, \`{project_knowledge}\` -- MD-based workflows execute directly — load and follow the \`.md\` file -- YAML-based workflows require the workflow engine — load \`workflow.xml\` first, then pass the \`.yaml\` config -- Follow step-based workflow execution: load steps JIT, never multiple at once -- Save outputs after EACH step when using the workflow engine -- The \`{project-root}\` variable resolves to the workspace root at runtime - -## Available Agents - -${agentsTable} -## Slash Commands - -Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent activators. Agents are also available in the agents dropdown.`; - - const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md'); - const markerStart = ''; - const markerEnd = ''; - const markedContent = `${markerStart}\n${bmadSection}\n${markerEnd}`; - - if (await fs.pathExists(instructionsPath)) { - const existing = await fs.readFile(instructionsPath, 'utf8'); - const startIdx = existing.indexOf(markerStart); - const endIdx = existing.indexOf(markerEnd); - - if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { - // Replace only the BMAD section between markers - const before = existing.slice(0, startIdx); - const after = existing.slice(endIdx + markerEnd.length); - const merged = `${before}${markedContent}${after}`; - await this.writeFile(instructionsPath, merged); - } else { - // Existing file without markers — back it up before overwriting - const backupPath = `${instructionsPath}.bak`; - await fs.copy(instructionsPath, backupPath); - if (!options.silent) await prompts.log.warn(` Backed up copilot-instructions.md → .bak`); - await this.writeFile(instructionsPath, `${markedContent}\n`); - } - } else { - // No existing file — create fresh with markers - await this.writeFile(instructionsPath, `${markedContent}\n`); - } - } - - /** - * Load module config.yaml for template variables - * @param {string} bmadDir - BMAD installation directory - * @returns {Object} Config variables - */ - async loadModuleConfig(bmadDir) { - const bmmConfigPath = path.join(bmadDir, 'bmm', 'config.yaml'); - const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml'); - - for (const configPath of [bmmConfigPath, coreConfigPath]) { - if (await fs.pathExists(configPath)) { - try { - const content = await fs.readFile(configPath, 'utf8'); - return yaml.parse(content) || {}; - } catch { - // Fall through to next config - } - } - } - - return {}; - } - - /** - * Escape a string for use inside YAML single-quoted values. - * In YAML, the only escape inside single quotes is '' for a literal '. - * @param {string} value - Raw string - * @returns {string} Escaped string safe for YAML single-quoted context - */ - escapeYamlSingleQuote(value) { - return (value || '').replaceAll("'", "''"); - } - - /** - * Scan existing agent and prompt files for customised tool permissions before cleanup. - * Returns a Map so permissions can be preserved across reinstalls. - * @param {string} projectDir - Project directory - * @returns {Map} Existing tool permissions keyed by filename - */ - async collectExistingToolPermissions(projectDir) { - const permissions = new Map(); - const dirs = [ - [path.join(projectDir, this.githubDir, this.agentsDir), /^bmad.*\.agent\.md$/], - [path.join(projectDir, this.githubDir, this.promptsDir), /^bmad-.*\.prompt\.md$/], - ]; - - for (const [dir, pattern] of dirs) { - if (!(await fs.pathExists(dir))) continue; - const files = await fs.readdir(dir); - - for (const file of files) { - if (!pattern.test(file)) continue; - - try { - const content = await fs.readFile(path.join(dir, file), 'utf8'); - const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); - if (!fmMatch) continue; - - const frontmatter = yaml.parse(fmMatch[1]); - if (frontmatter && Array.isArray(frontmatter.tools)) { - permissions.set(file, frontmatter.tools); - } - } catch { - // Skip unreadable files - } - } - } - - return permissions; - } - - /** - * Get the tools array string for a file, preserving any existing customisation. - * Falls back to the default tools if no prior customisation exists. - * @param {string} fileName - Target filename (e.g. 'bmad-agent-bmm-pm.agent.md') - * @returns {string} YAML inline array string - */ - getToolsForFile(fileName) { - const defaultTools = ['read', 'edit', 'search', 'execute']; - const tools = (this.existingToolPermissions && this.existingToolPermissions.get(fileName)) || defaultTools; - return '[' + tools.map((t) => `'${t}'`).join(', ') + ']'; - } - - /** - * Format name as title - */ - formatTitle(name) { - return name - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - /** - * Cleanup GitHub Copilot configuration - surgically remove only BMAD files - */ - async cleanup(projectDir, options = {}) { - // Clean up agents directory - const agentsDir = path.join(projectDir, this.githubDir, this.agentsDir); - if (await fs.pathExists(agentsDir)) { - const files = await fs.readdir(agentsDir); - let removed = 0; - - for (const file of files) { - if (file.startsWith('bmad') && (file.endsWith('.agent.md') || file.endsWith('.md'))) { - await fs.remove(path.join(agentsDir, file)); - removed++; - } - } - - if (removed > 0 && !options.silent) { - await prompts.log.message(` Cleaned up ${removed} existing BMAD agents`); - } - } - - // Clean up prompts directory - const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir); - if (await fs.pathExists(promptsDir)) { - const files = await fs.readdir(promptsDir); - let removed = 0; - - for (const file of files) { - if (file.startsWith('bmad-') && file.endsWith('.prompt.md')) { - await fs.remove(path.join(promptsDir, file)); - removed++; - } - } - - if (removed > 0 && !options.silent) { - await prompts.log.message(` Cleaned up ${removed} existing BMAD prompts`); - } - } - - // During uninstall, also strip BMAD markers from copilot-instructions.md. - // During reinstall (default), this is skipped because generateCopilotInstructions() - // handles marker-based replacement in a single read-modify-write pass, - // which correctly preserves user content outside the markers. - if (options.isUninstall) { - await this.cleanupCopilotInstructions(projectDir, options); - } - } - - /** - * Strip BMAD marker section from copilot-instructions.md - * If file becomes empty after stripping, delete it. - * If a .bak backup exists and the main file was deleted, restore the backup. - * @param {string} projectDir - Project directory - * @param {Object} [options] - Options (e.g. { silent: true }) - */ - async cleanupCopilotInstructions(projectDir, options = {}) { - const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md'); - const backupPath = `${instructionsPath}.bak`; - - if (!(await fs.pathExists(instructionsPath))) { - return; - } - - const content = await fs.readFile(instructionsPath, 'utf8'); - const markerStart = ''; - const markerEnd = ''; - const startIdx = content.indexOf(markerStart); - const endIdx = content.indexOf(markerEnd); - - if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) { - return; // No valid markers found - } - - // Strip the marker section (including markers) - const before = content.slice(0, startIdx); - const after = content.slice(endIdx + markerEnd.length); - const cleaned = before + after; - - if (cleaned.trim().length === 0) { - // File is empty after stripping — delete it - await fs.remove(instructionsPath); - - // If backup exists, restore it - if (await fs.pathExists(backupPath)) { - await fs.rename(backupPath, instructionsPath); - if (!options.silent) { - await prompts.log.message(' Restored copilot-instructions.md from backup'); - } - } - } else { - // Write cleaned content back (preserve original whitespace) - await fs.writeFile(instructionsPath, cleaned, 'utf8'); - - // If backup exists, it's stale now — remove it - if (await fs.pathExists(backupPath)) { - await fs.remove(backupPath); - } - } - } -} - -module.exports = { GitHubCopilotSetup }; diff --git a/tools/cli/installers/lib/ide/kilo.js b/tools/cli/installers/lib/ide/kilo.js deleted file mode 100644 index 2e5734391..000000000 --- a/tools/cli/installers/lib/ide/kilo.js +++ /dev/null @@ -1,269 +0,0 @@ -const path = require('node:path'); -const { BaseIdeSetup } = require('./_base-ide'); -const yaml = require('yaml'); -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'); - -/** - * KiloCode IDE setup handler - * Creates custom modes in .kilocodemodes file (similar to Roo) - */ -class KiloSetup extends BaseIdeSetup { - constructor() { - super('kilo', 'Kilo Code'); - this.configFile = '.kilocodemodes'; - } - - /** - * Setup KiloCode IDE configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - 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); - - // Load existing config (may contain non-BMAD modes and other settings) - const kiloModesPath = path.join(projectDir, this.configFile); - let config = {}; - - if (await this.pathExists(kiloModesPath)) { - const existingContent = await this.readFile(kiloModesPath); - try { - config = yaml.parse(existingContent) || {}; - } catch { - // If parsing fails, start fresh but warn user - await prompts.log.warn('Warning: Could not parse existing .kilocodemodes, starting fresh'); - config = {}; - } - } - - // Ensure customModes array exists - if (!Array.isArray(config.customModes)) { - config.customModes = []; - } - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Create mode objects and add to config - let addedCount = 0; - - for (const artifact of agentArtifacts) { - const modeObject = await this.createModeObject(artifact, projectDir); - config.customModes.push(modeObject); - addedCount++; - } - - // Write .kilocodemodes file with proper YAML structure - const finalContent = yaml.stringify(config, { lineWidth: 0 }); - await this.writeFile(kiloModesPath, finalContent); - - // Generate workflow commands - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - - // Write to .kilocode/workflows/ directory - const workflowsDir = path.join(projectDir, '.kilocode', 'workflows'); - await this.ensureDir(workflowsDir); - - // Clear old BMAD workflows before writing new ones - await this.clearBmadWorkflows(workflowsDir); - - // Write workflow files - const workflowCount = await workflowGenerator.writeDashArtifacts(workflowsDir, workflowArtifacts); - - // Generate task and tool commands - const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); - const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); - - // Write task/tool files to workflows directory (same location as workflows) - await taskToolGen.writeDashArtifacts(workflowsDir, taskToolArtifacts); - const taskCount = taskToolCounts.tasks || 0; - const toolCount = taskToolCounts.tools || 0; - - if (!options.silent) { - await prompts.log.success( - `${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`, - ); - } - - return { - success: true, - modes: addedCount, - workflows: workflowCount, - tasks: taskCount, - tools: toolCount, - }; - } - - /** - * Create a mode object for an agent - * @param {Object} artifact - Agent artifact - * @param {string} projectDir - Project directory - * @returns {Object} Mode object for YAML serialization - */ - async createModeObject(artifact, projectDir) { - // Extract metadata from launcher content - const titleMatch = artifact.content.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name); - - const iconMatch = artifact.content.match(/icon="([^"]+)"/); - const icon = iconMatch ? iconMatch[1] : '🤖'; - - const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/); - const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; - - // Get the activation header from central template (trim to avoid YAML formatting issues) - const activationHeader = (await this.getAgentCommandHeader()).trim(); - - const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/); - const roleDefinition = roleDefinitionMatch - ? roleDefinitionMatch[1] - : `You are a ${title} specializing in ${title.toLowerCase()} tasks.`; - - // Get relative path - const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/'); - - // Build mode object (KiloCode uses same schema as Roo) - return { - slug: `bmad-${artifact.module}-${artifact.name}`, - name: `${icon} ${title}`, - roleDefinition: roleDefinition, - whenToUse: whenToUse, - customInstructions: `${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`, - groups: ['read', 'edit', 'browser', 'command', 'mcp'], - }; - } - - /** - * Format name as title - */ - formatTitle(name) { - return name - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - /** - * Clear old BMAD workflow files from workflows directory - * @param {string} workflowsDir - Workflows directory path - */ - async clearBmadWorkflows(workflowsDir) { - const fs = require('fs-extra'); - if (!(await fs.pathExists(workflowsDir))) return; - - const entries = await fs.readdir(workflowsDir); - for (const entry of entries) { - if (entry.startsWith('bmad-') && entry.endsWith('.md')) { - await fs.remove(path.join(workflowsDir, entry)); - } - } - } - - /** - * Cleanup KiloCode configuration - */ - async cleanup(projectDir, options = {}) { - const fs = require('fs-extra'); - const kiloModesPath = path.join(projectDir, this.configFile); - - if (await fs.pathExists(kiloModesPath)) { - const content = await fs.readFile(kiloModesPath, 'utf8'); - - try { - const config = yaml.parse(content) || {}; - - if (Array.isArray(config.customModes)) { - const originalCount = config.customModes.length; - // Remove BMAD modes only (keep non-BMAD modes) - config.customModes = config.customModes.filter((mode) => !mode.slug || !mode.slug.startsWith('bmad-')); - const removedCount = originalCount - config.customModes.length; - - if (removedCount > 0) { - await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 })); - if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .kilocodemodes`); - } - } - } catch { - // If parsing fails, leave file as-is - if (!options.silent) await prompts.log.warn('Warning: Could not parse .kilocodemodes for cleanup'); - } - } - - // Clean up workflow files - const workflowsDir = path.join(projectDir, '.kilocode', 'workflows'); - await this.clearBmadWorkflows(workflowsDir); - } - - /** - * Install a custom agent launcher for Kilo - * @param {string} projectDir - Project directory - * @param {string} agentName - Agent name (e.g., "fred-commit-poet") - * @param {string} agentPath - Path to compiled agent (relative to project root) - * @param {Object} metadata - Agent metadata - * @returns {Object} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const kilocodemodesPath = path.join(projectDir, this.configFile); - let config = {}; - - // Read existing .kilocodemodes file - if (await this.pathExists(kilocodemodesPath)) { - const existingContent = await this.readFile(kilocodemodesPath); - try { - config = yaml.parse(existingContent) || {}; - } catch { - config = {}; - } - } - - // Ensure customModes array exists - if (!Array.isArray(config.customModes)) { - config.customModes = []; - } - - // Create custom agent mode object - const slug = `bmad-custom-${agentName.toLowerCase()}`; - - // Check if mode already exists - if (config.customModes.some((mode) => mode.slug === slug)) { - return { - ide: 'kilo', - path: this.configFile, - command: agentName, - type: 'custom-agent-launcher', - alreadyExists: true, - }; - } - - // Add custom mode object - config.customModes.push({ - slug: slug, - name: `BMAD Custom: ${agentName}`, - description: `Custom BMAD agent: ${agentName}\n\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\nThis is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.\n`, - prompt: `@${agentPath}\n`, - always: false, - permissions: 'all', - }); - - // Write .kilocodemodes file with proper YAML structure - await this.writeFile(kilocodemodesPath, yaml.stringify(config, { lineWidth: 0 })); - - return { - ide: 'kilo', - path: this.configFile, - command: slug, - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { KiloSetup }; diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 654574a25..908a094a3 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts'); * Dynamically discovers and loads IDE handlers * * Loading strategy: - * 1. Custom installer files (github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic + * 1. Custom installer files (rovodev.js) - for platforms with unique installation logic * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns */ class IdeManager { @@ -44,7 +44,7 @@ class IdeManager { /** * Dynamically load all IDE handlers - * 1. Load custom installer files first (github-copilot.js, kilo.js, rovodev.js) + * 1. Load custom installer files first (kilo.js, rovodev.js) * 2. Load config-driven handlers from platform-codes.yaml */ async loadHandlers() { @@ -58,11 +58,11 @@ class IdeManager { /** * Load custom installer files (unique installation logic) * These files have special installation patterns that don't fit the config-driven model - * Note: codex was migrated to config-driven (platform-codes.yaml) and no longer needs a custom installer + * Note: codex, github-copilot, and kilo were migrated to config-driven (platform-codes.yaml) */ async loadCustomInstallerFiles() { const ideDir = __dirname; - const customFiles = ['github-copilot.js', 'kilo.js', 'rovodev.js']; + const customFiles = ['rovodev.js']; for (const file of customFiles) { const filePath = path.join(ideDir, file); @@ -190,14 +190,6 @@ class IdeManager { if (r.tasks > 0) parts.push(`${r.tasks} tasks`); if (r.tools > 0) parts.push(`${r.tools} tools`); detail = parts.join(', '); - } else if (handlerResult && handlerResult.modes !== undefined) { - // Kilo handler returns { success, modes, workflows, tasks, tools } - const parts = []; - if (handlerResult.modes > 0) parts.push(`${handlerResult.modes} modes`); - if (handlerResult.workflows > 0) parts.push(`${handlerResult.workflows} workflows`); - if (handlerResult.tasks > 0) parts.push(`${handlerResult.tasks} tasks`); - if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`); - detail = parts.join(', '); } // Propagate handler's success status (default true for backward compat) const success = handlerResult?.success !== false; diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 99269552f..37497c86b 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -57,8 +57,11 @@ platforms: category: ide description: "AI coding assistant" installer: - target_dir: .clinerules/workflows - template_type: windsurf + legacy_targets: + - .clinerules/workflows + target_dir: .cline/skills + template_type: default + skill_format: true codex: name: "Codex" @@ -81,8 +84,11 @@ platforms: category: ide description: "Tencent Cloud Code Assistant - AI-powered coding companion" installer: - target_dir: .codebuddy/commands + legacy_targets: + - .codebuddy/commands + target_dir: .codebuddy/skills template_type: default + skill_format: true crush: name: "Crush" @@ -90,8 +96,11 @@ platforms: category: ide description: "AI development assistant" installer: - target_dir: .crush/commands + legacy_targets: + - .crush/commands + target_dir: .crush/skills template_type: default + skill_format: true cursor: name: "Cursor" @@ -119,7 +128,13 @@ platforms: preferred: false category: ide description: "GitHub's AI pair programmer" - # No installer config - uses custom github-copilot.js + installer: + legacy_targets: + - .github/agents + - .github/prompts + target_dir: .github/skills + template_type: default + skill_format: true iflow: name: "iFlow" @@ -135,7 +150,12 @@ platforms: preferred: false category: ide description: "AI coding platform" - # No installer config - uses custom kilo.js (creates .kilocodemodes file) + installer: + legacy_targets: + - .kilocode/workflows + target_dir: .kilocode/skills + template_type: default + skill_format: true kiro: name: "Kiro" @@ -180,8 +200,11 @@ platforms: category: ide description: "Enhanced Cline fork" installer: - target_dir: .roo/commands + legacy_targets: + - .roo/commands + target_dir: .roo/skills template_type: default + skill_format: true rovo-dev: name: "Rovo Dev" @@ -196,8 +219,11 @@ platforms: category: ide description: "AI coding tool" installer: - target_dir: .trae/rules - template_type: trae + legacy_targets: + - .trae/rules + target_dir: .trae/skills + template_type: default + skill_format: true windsurf: name: "Windsurf" diff --git a/tools/docs/native-skills-migration-checklist.md b/tools/docs/native-skills-migration-checklist.md index ba8f412ed..614871f99 100644 --- a/tools/docs/native-skills-migration-checklist.md +++ b/tools/docs/native-skills-migration-checklist.md @@ -15,25 +15,27 @@ This checklist now includes those completed platforms plus the remaining full-su Support assumption: full Agent Skills support. BMAD has already migrated from `.claude/commands` to `.claude/skills`. -- [ ] Confirm current implementation still matches Claude Code skills expectations -- [ ] Confirm legacy cleanup for `.claude/commands` -- [ ] Test fresh install -- [ ] Test reinstall/upgrade from legacy command output -- [ ] Confirm ancestor conflict protection -- [ ] Implement/extend automated tests as needed -- [ ] Commit any follow-up fixes if required +**Install:** `npm install -g @anthropic-ai/claude-code` or `brew install claude-code` + +- [x] Confirm current implementation still matches Claude Code skills expectations +- [x] Confirm legacy cleanup for `.claude/commands` +- [x] Test fresh install +- [x] Test reinstall/upgrade from legacy command output +- [x] Confirm ancestor conflict protection because Claude Code inherits skills from parent directories and `ancestor_conflict_check: true` is set in platform-codes.yaml +- [x] Implement/extend automated tests as needed ## Codex CLI Support assumption: full Agent Skills support. BMAD has already migrated from `.codex/prompts` to `.agents/skills`. -- [ ] Confirm current implementation still matches Codex CLI skills expectations -- [ ] Confirm legacy cleanup for project and global `.codex/prompts` -- [ ] Test fresh install -- [ ] Test reinstall/upgrade from legacy prompt output +**Install:** `npm install -g @openai/codex` + +- [x] Confirm current implementation still matches Codex CLI skills expectations +- [x] Confirm legacy cleanup for project and global `.codex/prompts` +- [x] Test fresh install +- [x] Test reinstall/upgrade from legacy prompt output - [x] Confirm ancestor conflict protection because Codex inherits parent-directory `.agents/skills` -- [ ] Implement/extend automated tests as needed -- [ ] Commit any follow-up fixes if required +- [x] Implement/extend automated tests as needed ## Cursor @@ -45,7 +47,7 @@ Support assumption: full Agent Skills support. BMAD currently installs legacy co - [x] Test fresh install - [x] Test reinstall/upgrade from legacy command output - [x] Confirm no ancestor conflict protection is needed because a child workspace surfaced child `.cursor/skills` entries but not a parent-only skill during manual verification -- [ ] Implement/extend automated tests +- [x] Implement/extend automated tests - [x] Commit ## Windsurf @@ -59,20 +61,21 @@ Support assumption: full Agent Skills support. Windsurf docs confirm workspace s - [x] Test reinstall/upgrade from legacy workflow output - [x] Confirm no ancestor conflict protection is needed because manual Windsurf verification showed child-local `@` skills loaded while a parent-only skill was not inherited - [x] Implement/extend automated tests -- [x] Commit ## Cline -Support assumption: full Agent Skills support. BMAD currently installs workflow files to `.clinerules/workflows`; target should move to the platform's native skills directory. +Support assumption: full Agent Skills support. Cline docs confirm workspace skills at `.cline/skills//SKILL.md` and global skills at `~/.cline/skills/`. BMAD has now migrated from `.clinerules/workflows` to `.cline/skills`. -- [ ] Confirm current Cline skills path and whether `.cline/skills` is the correct BMAD target -- [ ] Implement installer migration to native skills output -- [ ] Add legacy cleanup for `.clinerules/workflows` -- [ ] Test fresh install -- [ ] Test reinstall/upgrade from legacy workflow output -- [ ] Confirm ancestor conflict protection where applicable -- [ ] Implement/extend automated tests -- [ ] Commit +**Install:** VS Code extension `saoudrizwan.claude-dev` — search "Cline" in Extensions or `code --install-extension saoudrizwan.claude-dev` + +- [x] Confirm current Cline skills path is `.cline/skills/{skill-name}/SKILL.md` with YAML frontmatter (name + description) +- [x] Implement installer migration to native skills output +- [x] Add legacy cleanup for `.clinerules/workflows` +- [x] Test fresh install — 43 skills installed to `.cline/skills/` +- [x] Test reinstall/upgrade from legacy workflow output +- [x] Confirm no ancestor conflict protection is needed because Cline only scans workspace-local `.cline/skills/` and global `~/.cline/skills/`, with no ancestor directory inheritance +- [x] Implement/extend automated tests — 9 assertions in test suite 18 +- [x] Commit ## Google Antigravity @@ -85,7 +88,6 @@ Support assumption: full Agent Skills support. Antigravity docs confirm workspac - [x] Test reinstall/upgrade from legacy workflow output - [x] Confirm no ancestor conflict protection is needed because manual Antigravity verification in `/tmp/antigravity-ancestor-repro/parent/child` showed only the child-local `child-only` skill, with no inherited parent `.agent/skills` entry - [x] Implement/extend automated tests -- [x] Commit ## Auggie @@ -98,33 +100,38 @@ Support assumption: full Agent Skills support. BMAD currently installs commands - [x] Test reinstall/upgrade from legacy command output - [x] Confirm no ancestor conflict protection is needed because local `auggie --workspace-root` repro showed child-local `.augment/skills` loading `child-only` but not parent `parent-only` - [x] Implement/extend automated tests -- [ ] Commit +- [x] Commit ## CodeBuddy -Support assumption: full Agent Skills support. BMAD currently installs commands to `.codebuddy/commands`; target should move to `.codebuddy/skills`. +Support assumption: full Agent Skills support. CodeBuddy docs confirm workspace skills at `.codebuddy/skills//SKILL.md` and global skills at `~/.codebuddy/commands/`. BMAD has now migrated from `.codebuddy/commands` to `.codebuddy/skills`. -- [ ] Confirm CodeBuddy native skills path and any naming/frontmatter requirements -- [ ] Implement installer migration to native skills output -- [ ] Add legacy cleanup for `.codebuddy/commands` -- [ ] Test fresh install -- [ ] Test reinstall/upgrade from legacy command output -- [ ] Confirm ancestor conflict protection where applicable -- [ ] Implement/extend automated tests -- [ ] Commit +**Install:** Download [Tencent CodeBuddy IDE](https://codebuddyide.net/) or install as VS Code extension `CodebuddyAI.codebuddy-ai` + +- [x] Confirm CodeBuddy native skills path is `.codebuddy/skills/{skill-name}/SKILL.md` with YAML frontmatter (name + description) — per docs, not IDE-verified +- [x] Implement installer migration to native skills output +- [x] Add legacy cleanup for `.codebuddy/commands` +- [x] Test fresh install — 43 skills installed to `.codebuddy/skills/` (installer output only) +- [x] Test reinstall/upgrade from legacy command output +- [ ] **NEEDS MANUAL IDE VERIFICATION** — requires Tencent Cloud account; confirm skills appear in UI and test ancestor inheritance +- [x] Implement/extend automated tests — 9 assertions in test suite 19 +- [x] Commit ## Crush -Support assumption: full Agent Skills support. BMAD currently installs commands to `.crush/commands`; target should move to the platform's native skills location. +Support assumption: full Agent Skills support. Crush scans project-local `.crush/skills/` exclusively ([GitHub issue #2072](https://github.com/charmbracelet/crush/issues/2072) confirms this and requests adding `~/.agents/skills/`). BMAD has now migrated from `.crush/commands` to `.crush/skills`. -- [ ] Confirm Crush project-local versus global skills path and BMAD's preferred install target -- [ ] Implement installer migration to native skills output -- [ ] Add legacy cleanup for `.crush/commands` -- [ ] Test fresh install -- [ ] Test reinstall/upgrade from legacy command output -- [ ] Confirm ancestor conflict protection where applicable -- [ ] Implement/extend automated tests -- [ ] Commit +**Install:** `brew install charmbracelet/tap/crush` (macOS/Linux) or `winget install charmbracelet.crush` (Windows) + +- [x] Confirm Crush project-local skills path is `.crush/skills/{skill-name}/SKILL.md` — per GitHub issue #2072 confirming `.crush/skills/` is the only scan path +- [x] Implement installer migration to native skills output +- [x] Add legacy cleanup for `.crush/commands` +- [x] Test fresh install — 43 skills installed to `.crush/skills/` +- [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 +- [ ] **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] Commit ## Kiro @@ -137,7 +144,6 @@ Support assumption: full Agent Skills support. Kiro docs confirm project skills - [x] Test reinstall/upgrade from legacy steering output - [x] Confirm no ancestor conflict protection is needed because manual Kiro verification showed Slash-visible skills from the current workspace only, with no ancestor `.kiro/skills` inheritance - [x] Implement/extend automated tests -- [x] Commit ## OpenCode @@ -150,60 +156,69 @@ Support assumption: full Agent Skills support. BMAD currently splits output betw - [x] Test reinstall/upgrade from split legacy output - [x] Confirm ancestor conflict protection is required because local `opencode run` repros loaded both child-local `child-only` and ancestor `parent-only`, matching the docs that project-local skill discovery walks upward to the git worktree - [x] Implement/extend automated tests -- [ ] Commit +- [x] Commit ## Roo Code Support assumption: full Agent Skills support. BMAD currently installs commands to `.roo/commands`; target should move to `.roo/skills` or the correct mode-aware skill directories. -- [ ] Confirm Roo native skills path and whether BMAD should use generic or mode-specific skill directories -- [ ] Implement installer migration to native skills output -- [ ] Add legacy cleanup for `.roo/commands` -- [ ] Test fresh install -- [ ] Test reinstall/upgrade from legacy command output -- [ ] Confirm ancestor conflict protection where applicable -- [ ] Implement/extend automated tests -- [ ] Commit +**Install:** VS Code extension `RooVeterinaryInc.roo-cline` — search "Roo Code" in Extensions or `code --install-extension RooVeterinaryInc.roo-cline` + +- [x] Confirm Roo native skills path is `.roo/skills/{skill-name}/SKILL.md` with `name` frontmatter matching directory exactly (lowercase, alphanumeric + hyphens only) +- [x] Implement installer migration to native skills output +- [x] Add legacy cleanup for `.roo/commands` +- [x] Test fresh install — 43 skills installed, verified in Roo Code v3.51 +- [x] Test reinstall/upgrade from legacy command output +- [x] Confirm no ancestor conflict protection is needed because manual Roo Code v3.51 verification showed child-local `child-only` skill loaded while parent-only skill was not inherited +- [x] Implement/extend automated tests — 7 assertions in test suite 13 +- [x] Commit ## Trae -Support assumption: full Agent Skills support. BMAD currently installs rule files to `.trae/rules`; target should move to the platform's native skills directory. +Support assumption: full Agent Skills support. [Trae docs](https://docs.trae.ai/ide/skills) confirm workspace skills at `.trae/skills//SKILL.md`. BMAD has now migrated from `.trae/rules` to `.trae/skills`. -- [ ] Confirm Trae native skills path and whether the current `.trae/rules` path is still required for compatibility -- [ ] Implement installer migration to native skills output -- [ ] Add legacy cleanup for `.trae/rules` -- [ ] Test fresh install -- [ ] Test reinstall/upgrade from legacy rules output -- [ ] Confirm ancestor conflict protection where applicable -- [ ] Implement/extend automated tests -- [ ] Commit +**Install:** Download [standalone IDE](https://www.trae.ai/download) (macOS/Windows/Linux) or `winget install -e --id ByteDance.Trae` + +- [x] Confirm Trae native skills path is `.trae/skills/{skill-name}/SKILL.md` — per official docs +- [x] Implement installer migration to native skills output +- [x] Add legacy cleanup for `.trae/rules` +- [x] Test fresh install — 43 skills installed to `.trae/skills/` +- [x] Test reinstall/upgrade from legacy rules output +- [x] Confirm no ancestor conflict protection is needed — Trae docs describe project-local `.trae/skills/` only +- [ ] **NEEDS MANUAL IDE VERIFICATION** — download Trae IDE and confirm skills appear in UI +- [x] Implement/extend automated tests — 9 assertions in test suite 21 +- [x] Commit ## GitHub Copilot Support assumption: full Agent Skills support. BMAD currently uses a custom installer that generates `.github/agents`, `.github/prompts`, and `.github/copilot-instructions.md`; target should move to `.github/skills`. -- [ ] Confirm GitHub Copilot native skills path and whether `.github/agents` remains necessary as a compatibility layer -- [ ] Design the migration away from the custom prompt/agent installer model -- [ ] Implement native skills output, ideally with shared config-driven code where practical -- [ ] Add legacy cleanup for `.github/agents`, `.github/prompts`, and any BMAD-owned Copilot instruction file behavior that should be retired -- [ ] Test fresh install -- [ ] Test reinstall/upgrade from legacy custom installer output -- [ ] Confirm ancestor conflict protection where applicable -- [ ] Implement/extend automated tests -- [ ] Commit +**Install:** VS Code extension `GitHub.copilot` — search "GitHub Copilot" in Extensions or `code --install-extension GitHub.copilot` + +- [x] Confirm GitHub Copilot native skills path is `.github/skills/{skill-name}/SKILL.md` — also reads `.claude/skills/` automatically +- [x] Design the migration away from the custom prompt/agent installer model — replaced 699-line custom installer with config-driven `skill_format: true` +- [x] Implement native skills output, ideally with shared config-driven code where practical +- [x] Add legacy cleanup for `.github/agents`, `.github/prompts`, and BMAD markers in `copilot-instructions.md` +- [x] Test fresh install — 43 skills installed to `.github/skills/` +- [x] Test reinstall/upgrade from legacy custom installer output — legacy dirs removed, BMAD markers stripped, user content preserved +- [x] Confirm no ancestor conflict protection is needed because manual Copilot verification showed child-local `child-only` skill loaded while parent-only skill was not inherited +- [x] Implement/extend automated tests — 11 assertions in test suite 17 including marker cleanup +- [x] Commit ## KiloCoder 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. -- [ ] Confirm KiloCoder native skills path and whether `.kilocodemodes` should be removed entirely or retained temporarily for compatibility -- [ ] Design the migration away from modes plus workflow markdown -- [ ] Implement native skills output -- [ ] Add legacy cleanup for `.kilocode/workflows` and BMAD-owned entries in `.kilocodemodes` -- [ ] Test fresh install -- [ ] Test reinstall/upgrade from legacy custom installer output -- [ ] Confirm ancestor conflict protection where applicable -- [ ] Implement/extend automated tests +**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 is `.kilocode/skills/{skill-name}/SKILL.md` (Kilo forked from Roo Code which uses `.roo/skills/`) +- [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] Implement native skills output — target_dir `.kilocode/skills`, skill_format true, template_type default +- [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] 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 +- [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) - [ ] Commit ## Summary Gates