From e28eaf76775b18f7f688c3041c8fc1b342606335 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sat, 7 Mar 2026 04:18:37 -0700 Subject: [PATCH] fix(installer): preserve bmad-os-* skills during cleanup The cleanupTarget method removed all entries starting with "bmad" from IDE skills directories, which would also wipe version-controlled bmad-os-* skills from the BMAD-METHOD repo. Add exclusion for the bmad-os- prefix so those skills survive reinstalls. --- test/test-installation-components.js | 62 +++++++++++++++++++ .../cli/installers/lib/ide/_config-driven.js | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/test/test-installation-components.js b/test/test-installation-components.js index eac17e72e..d04d5d33f 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -1428,6 +1428,68 @@ async function runTests() { console.log(''); + // ============================================================ + // Suite 27: Cleanup preserves bmad-os-* skills + // ============================================================ + console.log(`${colors.yellow}Test Suite 27: Cleanup preserves bmad-os-* skills${colors.reset}\n`); + + try { + const tempProjectDir27 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-os-preserve-test-')); + const installedBmadDir27 = await createTestBmadFixture(); + + // Pre-populate .claude/skills with bmad-os-* skills (version-controlled repo skills) + const osSkillDir27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-os-review-pr'); + await fs.ensureDir(osSkillDir27); + await fs.writeFile( + path.join(osSkillDir27, 'SKILL.md'), + '---\nname: bmad-os-review-pr\ndescription: Review PRs\n---\nOS skill content\n', + ); + + const osSkillDir27b = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-os-release-module'); + await fs.ensureDir(osSkillDir27b); + await fs.writeFile( + path.join(osSkillDir27b, 'SKILL.md'), + '---\nname: bmad-os-release-module\ndescription: Release module\n---\nOS skill content\n', + ); + + // Also add a regular bmad skill that SHOULD be cleaned up + const regularSkillDir27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-architect'); + await fs.ensureDir(regularSkillDir27); + await fs.writeFile( + path.join(regularSkillDir27, 'SKILL.md'), + '---\nname: bmad-architect\ndescription: Architect\n---\nOld skill content\n', + ); + + // Run Claude Code setup (which triggers cleanup then install) + const ideManager27 = new IdeManager(); + await ideManager27.ensureInitialized(); + const result27 = await ideManager27.setup('claude-code', tempProjectDir27, installedBmadDir27, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result27.success === true, 'Claude Code setup succeeds with bmad-os-* skills present'); + + // bmad-os-* skills must survive + assert(await fs.pathExists(osSkillDir27), 'Cleanup preserves bmad-os-review-pr skill'); + assert(await fs.pathExists(osSkillDir27b), 'Cleanup preserves bmad-os-release-module skill'); + + // bmad-os skill content must be untouched + const osContent27 = await fs.readFile(path.join(osSkillDir27, 'SKILL.md'), 'utf8'); + assert(osContent27.includes('OS skill content'), 'bmad-os-review-pr skill content is unchanged'); + + // Regular bmad skill should have been replaced by fresh install + const newSkillFile27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(newSkillFile27), 'Fresh bmad skills are installed alongside preserved bmad-os-* skills'); + + await fs.remove(tempProjectDir27); + await fs.remove(installedBmadDir27); + } catch (error) { + assert(false, 'bmad-os-* skill preservation test succeeds', error.message); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index e4c29402f..325848c77 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -756,7 +756,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} if (!entry || typeof entry !== 'string') { continue; } - if (entry.startsWith('bmad')) { + if (entry.startsWith('bmad') && !entry.startsWith('bmad-os-')) { const entryPath = path.join(targetPath, entry); try { await fs.remove(entryPath);