diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 177fca166..6e015322a 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -3278,8 +3278,9 @@ async function runTests() { // ============================================================ console.log(`${colors.yellow}Test Suite 45: cleanup prunes empty skill-group dirs${colors.reset}\n`); + let root45; try { - const root45 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cleanup-test-')); + root45 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cleanup-test-')); const bmadDir45 = path.join(root45, '_bmad'); await fs.ensureDir(path.join(bmadDir45, '_config')); @@ -3307,12 +3308,12 @@ async function runTests() { 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++; + } finally { + if (root45) await fs.remove(root45).catch(() => {}); } console.log(''); diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 4898873b6..df9955f40 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -426,17 +426,24 @@ class Installer { /** * Remove now-empty parent directories left behind after skill dir cleanup. - * Walks up from dir, stopping at (and never removing) bmadDir. + * Walks up from dir, stopping at (and never removing) bmadDir. Best-effort: + * a directory that vanishes or fills in mid-walk just ends the walk. * @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); + while (true) { + // Path-boundary check (not a string prefix, so siblings like _bmad2 don't match). + const rel = path.relative(bmadDir, current); + if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) break; + try { + const entries = await fs.readdir(current); + if (entries.length > 0) break; + await fs.rmdir(current); + } catch { + break; + } current = path.dirname(current); } }