98 lines
3.7 KiB
JavaScript
98 lines
3.7 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { EXIT, BmadModuleError } from './lib/exit.mjs';
|
|
import { findBmadDir } from './lib/bmad-dir.mjs';
|
|
import { pruneEmptyDirs } from './lib/fs-safe.mjs';
|
|
import {
|
|
readManifestYaml,
|
|
removeModuleFromManifest,
|
|
removeSkillManifestRows,
|
|
removeFilesManifestRows,
|
|
readFileEntriesForModule,
|
|
readSkillCanonicalIdsForModule,
|
|
} from './lib/manifest-ops.mjs';
|
|
import { distributeToIdes } from './lib/ide-sync.mjs';
|
|
|
|
// Remove a module's installed files and manifest entries. With `--purge` also
|
|
// deletes `_bmad/custom/<code>/` (user customization dir). Without it, customs
|
|
// are preserved so a re-install picks them back up.
|
|
export async function runRemove(opts) {
|
|
const projectDir = opts.projectDir || process.cwd();
|
|
const code = opts.code;
|
|
if (!code) throw new BmadModuleError(EXIT.USAGE, `bmad-module remove <code> is required`);
|
|
|
|
const bmadDir = await findBmadDir(projectDir);
|
|
if (!bmadDir) {
|
|
throw new BmadModuleError(EXIT.NO_BMAD_DIR, `no _bmad/ found in ${projectDir}`);
|
|
}
|
|
|
|
const manifest = await readManifestYaml(bmadDir);
|
|
const entry = manifest?.modules?.find((m) => m && m.name === code);
|
|
if (!entry) {
|
|
throw new BmadModuleError(EXIT.NOT_INSTALLED, `no module "${code}" in manifest.yaml`);
|
|
}
|
|
if (entry.source !== 'community') {
|
|
throw new BmadModuleError(
|
|
EXIT.PREFIX_COLLISION,
|
|
`module "${code}" was installed as source="${entry.source}", not "community". ` +
|
|
`Use the appropriate uninstaller (e.g. \`bmad-method uninstall\`).`,
|
|
);
|
|
}
|
|
|
|
// Capture the module's distributed skill ids before dropping its manifest
|
|
// rows, so we can prune them from the IDE directories afterward.
|
|
const removedSkillIds = await readSkillCanonicalIdsForModule(bmadDir, code);
|
|
|
|
// Delete each file tracked in files-manifest.csv; prune empty dirs after.
|
|
const fileEntries = await readFileEntriesForModule(bmadDir, code);
|
|
const moduleRoot = path.join(bmadDir, code);
|
|
for (const fe of fileEntries) {
|
|
const abs = path.join(bmadDir, fe.path);
|
|
try {
|
|
await fs.rm(abs, { force: true });
|
|
await pruneEmptyDirs(path.dirname(abs), moduleRoot);
|
|
} catch (e) {
|
|
process.stderr.write(`[bmad-module] warn: failed to remove ${fe.path}: ${e.message}\n`);
|
|
}
|
|
}
|
|
|
|
// Remove the module root if it still exists (in case files-manifest was
|
|
// incomplete or empty). Safe — at this point we've confirmed source=community.
|
|
await fs.rm(moduleRoot, { recursive: true, force: true });
|
|
|
|
// Optionally purge custom overrides.
|
|
if (opts.purge) {
|
|
const customDir = path.join(bmadDir, 'custom', code);
|
|
await fs.rm(customDir, { recursive: true, force: true });
|
|
}
|
|
|
|
// Drop manifest rows.
|
|
await removeFilesManifestRows(bmadDir, code);
|
|
await removeSkillManifestRows(bmadDir, code);
|
|
await removeModuleFromManifest(bmadDir, code);
|
|
|
|
// Prune the module's skills from every configured coding assistant. The
|
|
// manifest no longer lists the module, so ide-sync removes its skill dirs +
|
|
// command pointers and re-syncs the rest.
|
|
const ideResult = await distributeToIdes({ projectDir, bmadDir, prune: removedSkillIds });
|
|
if (!ideResult.skipped && !ideResult.ok) {
|
|
process.stderr.write(`[bmad-module] warning: ${ideResult.hint}\n`);
|
|
}
|
|
|
|
process.stdout.write(`[bmad-module] removed ${code} (${fileEntries.length} file(s))\n`);
|
|
if (opts.purge) {
|
|
process.stdout.write(`[bmad-module] purged _bmad/custom/${code}/\n`);
|
|
} else if (await dirExists(path.join(bmadDir, 'custom', code))) {
|
|
process.stdout.write(`[bmad-module] preserved _bmad/custom/${code}/ (use --purge to remove)\n`);
|
|
}
|
|
}
|
|
|
|
async function dirExists(p) {
|
|
try {
|
|
const s = await fs.stat(p);
|
|
return s.isDirectory();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|