fix: remove empty skill-group dirs left in _bmad after install

Skill cleanup removed each skill's own directory but never pruned the
now-empty grouping folders above it (e.g. _bmad/bmm/1-analysis), leaving
empty dirs behind after every install. Walk up from each removed skill
dir and drop empty parents, stopping at the bmad root.
This commit is contained in:
Dov Benyomin Sohacheski 2026-06-10 05:14:54 +03:00
parent 397b2a5c87
commit d4f377f12d
No known key found for this signature in database
2 changed files with 62 additions and 0 deletions

View File

@ -3273,6 +3273,50 @@ async function runTests() {
console.log('');
// ============================================================
// Test Suite 45: _cleanupSkillDirs prunes empty parent dirs (#empty-bmm-folders)
// ============================================================
console.log(`${colors.yellow}Test Suite 45: cleanup prunes empty skill-group dirs${colors.reset}\n`);
try {
const root45 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cleanup-test-'));
const bmadDir45 = path.join(root45, '_bmad');
await fs.ensureDir(path.join(bmadDir45, '_config'));
// Two skills nested under the same grouping dir (1-analysis), plus a
// module-level file that must survive the cleanup.
await fs.writeFile(
path.join(bmadDir45, '_config', 'skill-manifest.csv'),
[
'canonicalId,name,description,module,path',
'"bmad-agent-analyst","bmad-agent-analyst","fixture","bmm","_bmad/bmm/1-analysis/bmad-agent-analyst/SKILL.md"',
'"bmad-research","bmad-research","fixture","bmm","_bmad/bmm/1-analysis/research/bmad-research/SKILL.md"',
'',
].join('\n'),
);
await fs.ensureDir(path.join(bmadDir45, 'bmm', '1-analysis', 'bmad-agent-analyst'));
await fs.writeFile(path.join(bmadDir45, 'bmm', '1-analysis', 'bmad-agent-analyst', 'SKILL.md'), 'x');
await fs.ensureDir(path.join(bmadDir45, 'bmm', '1-analysis', 'research', 'bmad-research'));
await fs.writeFile(path.join(bmadDir45, 'bmm', '1-analysis', 'research', 'bmad-research', 'SKILL.md'), 'x');
await fs.writeFile(path.join(bmadDir45, 'bmm', 'config.yaml'), 'module: bmm\n');
const installer45 = new Installer();
await installer45._cleanupSkillDirs(bmadDir45);
assert(!(await fs.pathExists(path.join(bmadDir45, 'bmm', '1-analysis'))), 'empty skill-group dir is pruned after cleanup');
assert(!(await fs.pathExists(path.join(bmadDir45, 'bmm', '1-analysis', 'research'))), 'empty nested skill-group dir is pruned');
assert(await fs.pathExists(path.join(bmadDir45, 'bmm', 'config.yaml')), 'module-level files are preserved');
assert(await fs.pathExists(bmadDir45), 'bmad root is never removed');
await fs.remove(root45);
} catch (error) {
console.log(`${colors.red}Test Suite 45 setup failed: ${error.message}${colors.reset}`);
console.log(error.stack);
failed++;
}
console.log('');
// ============================================================
// Summary
// ============================================================

View File

@ -419,10 +419,28 @@ class Installer {
const sourceDir = path.dirname(path.join(bmadDir, relativePath));
if (await fs.pathExists(sourceDir)) {
await fs.remove(sourceDir);
await this._removeEmptyParents(path.dirname(sourceDir), bmadDir);
}
}
}
/**
* Remove now-empty parent directories left behind after skill dir cleanup.
* Walks up from dir, stopping at (and never removing) bmadDir.
* @param {string} dir - Directory to start walking up from
* @param {string} bmadDir - BMAD installation directory (boundary)
*/
async _removeEmptyParents(dir, bmadDir) {
let current = dir;
while (current.startsWith(bmadDir) && current !== bmadDir) {
if (!(await fs.pathExists(current))) break;
const entries = await fs.readdir(current);
if (entries.length > 0) break;
await fs.rmdir(current);
current = path.dirname(current);
}
}
async _readSkillManifestRows(bmadDir) {
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
if (!(await fs.pathExists(csvPath))) return [];