Compare commits

...

4 Commits

Author SHA1 Message Date
gabadi e9ac52e949
Merge d80d05ad30 into c46502f640 2026-04-07 07:30:23 -07:00
Brian c46502f640
feat(installer): overhaul branding, versioning, and skill cleanup (#2223)
* feat(installer): overhaul branding, versioning, and skill cleanup

Logo and branding:
- Responsive logo: full "BMAD METHOD" at >=95 cols, "BMAD" for narrower terminals
- Color scheme updated from yellow to blue (matching bmadcode.com brand)
- Added copyright notice and tagline in white for contrast
- Removed version number from logo (individual module versions shown in summary)
- Added ™ to both wide and narrow logo variants

Installer start message:
- Replaced outdated V6 launch announcement with clean welcome
- Consolidated redundant module/platform messaging into single intro
- Tightened open source manifesto (same spirit, fewer words)
- Merged speaking/media into support section with contact email
- Added full social links: Website, Discord, YouTube, X, Facebook
- Replaced docs.bmad-method.org and changelog links with bmadcode.com hub

Install summary improvements:
- Module names now show full display names from module.yaml (not abbreviations)
- All module versions sourced from .claude-plugin/marketplace.json exclusively
- Summary shows version transitions: "v6.2.2 -> v6.3.0", "v6.3.0, no change",
  or "v6.3.0, installed" for fresh installs
- Switched summary from clack note() to box() for full-brightness text
- Removed dim/gray styling that was hard to read on dark terminals
- Links styled with color.blue instead of color.dim
- Get started section leads with actionable steps (launch agent, run bmad-help)
- Removed redundant social links (already shown in start message)

Version source unification:
- All module versions now come from .claude-plugin/marketplace.json only
- Removed package.json as version source for core/bmm modules
- Updated manifest.js getModuleVersionInfo() to use marketplace.json
- Updated installer.js _getMarketplaceVersion() helper
- Updated ui.js getMarketplaceVersion() for module selection display
- Quick Update menu no longer shows misleading version (was using package.json)
- Module selection list now shows versions next to each module name

Skill cleanup overhaul:
- Replaced blunt-force bmad-* prefix deletion with surgical removal system
- Added removals.txt support: optional per-project file listing skills to remove
- Created initial removals.txt with all skills removed since v6.2.0
- Install/update: captures previously installed skill IDs from skill-manifest.csv
  before manifest regeneration, then removes those + removals.txt entries
- Uninstall: removes all installed skills via skill-manifest.csv + removals.txt
- Deselecting modules now correctly removes their skills from IDE directories
- User-created bmad-* skills in IDE directories are no longer destroyed
- Legacy directory cleanup retains prefix matching (those dirs are abandoned)

Bug fixes:
- Fixed duplicate "CORE module already up to date" during quick update
- Fixed version display showing package.json version instead of actual module version
- Updated test fixture for bmad-os-* preservation test to use skill-manifest.csv

* fix(installer): address Augment review findings

- Fix plugins[0] fragility: extract highest version across all plugins
  in marketplace.json instead of assuming first entry (ui.js, installer.js,
  manifest.js)
- Fix _readMarketplaceVersion ignoring moduleSourcePath: custom modules
  can now source their own marketplace.json by walking up from source path
- Hard-exclude bmad-os-* utility skills in both surgical and legacy cleanup
  modes, preventing accidental deletion if tracked in manifests
- Distinguish missing file vs parse error in skill-manifest.csv reading:
  warn on corrupt CSV instead of silently skipping cleanup

* fix(installer): resolve module source before reading marketplace version

Move _readMarketplaceVersion call after source type resolution so custom
modules use their own source path instead of falling back to the external
module cache, which could match a different module with the same code.
2026-04-07 02:31:36 -05:00
2-gabadi d80d05ad30
fix: remove redundant editorial clause from self-review instruction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:47:46 -03:00
2-gabadi 398860fb07
fix: quick-dev self-review verifies AC coverage against parent artifact requirements
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:40:57 -03:00
10 changed files with 423 additions and 131 deletions

17
removals.txt Normal file
View File

@ -0,0 +1,17 @@
# BMad Method - Skill Removal List
# Entries listed here will be removed from IDE skill directories during install/update.
# One entry per line. Lines starting with # are comments.
# Each entry is a skill directory name (canonicalId) that was removed or renamed.
# Removed agents (v6.2.0 - v6.2.2)
bmad-agent-sm
bmad-agent-qa
bmad-agent-quick-flow-solo-dev
# Removed skills (v6.2.0 - v6.2.2)
bmad-create-product-brief
bmad-product-brief-preview
bmad-quick-spec
bmad-quick-flow
bmad-quick-dev-new-preview
bmad-init

View File

@ -54,7 +54,8 @@ context: [] # optional: `{project-root}/`-prefixed paths to project-wide standar
<!-- Tasks: backtick-quoted file path -- action -- rationale. Prefer one task per file; group tightly-coupled changes when splitting would be artificial. -->
<!-- If an I/O Matrix is present, include a task to unit-test its edge cases. -->
<!-- AC covers system-level behaviors not captured by the I/O Matrix. Do not duplicate I/O scenarios here. -->
<!-- AC covers system-level behaviors not captured by the I/O Matrix. Do not duplicate I/O scenarios here.
If requirements were assigned from a parent artifact, every assigned requirement must have an AC here. -->
**Execution:**
- [ ] `FILE` -- ACTION -- RATIONALE

View File

@ -14,7 +14,7 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
1. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
2. Read `./spec-template.md` fully. Fill it out based on the intent and investigation, and write the result to `{wipFile}`.
3. Self-review against READY FOR DEVELOPMENT standard.
3. Self-review against READY FOR DEVELOPMENT standard. If the intent derives from a parent artifact (epic plan, PRD, story brief), verify that every requirement assigned to this scope has a corresponding AC.
4. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
5. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
- Show user the token count.

View File

@ -1301,6 +1301,14 @@ async function runTests() {
'---\nname: bmad-architect\ndescription: Architect\n---\nOld skill content\n',
);
// Add bmad-architect to the existing skill-manifest.csv so cleanup knows it was previously installed
const configDir27 = path.join(installedBmadDir27, '_config');
const existingCsv27 = await fs.readFile(path.join(configDir27, 'skill-manifest.csv'), 'utf8');
await fs.writeFile(
path.join(configDir27, 'skill-manifest.csv'),
existingCsv27.trimEnd() + '\n"bmad-architect","bmad-architect","Architect","bmm","_bmad/bmm/agents/bmad-architect/SKILL.md","true"\n',
);
// Run Claude Code setup (which triggers cleanup then install)
const ideManager27 = new IdeManager();
await ideManager27.ensureInitialized();

View File

@ -19,24 +19,33 @@ const CLIUtils = {
* Display BMAD logo and version using @clack intro + box
*/
async displayLogo() {
const version = this.getVersion();
const color = await prompts.getColor();
const termWidth = process.stdout.columns || 80;
// ASCII art logo
const logo = [
// Full "BMad Method" logo for wide terminals, "BMad" only for narrow
const logoWide = [
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ™',
'██╔══██╗████╗ ████║██╔══██╗██╔══██╗ ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗',
'██████╔╝██╔████╔██║███████║██║ ██║ ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║',
'██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║ ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║',
'██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝',
'╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ',
];
const logoNarrow = [
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
' ██████╔╝██╔████╔██║███████║██║ ██║',
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
]
.map((line) => color.yellow(line))
.join('\n');
];
const tagline = ' Build More, Architect Dreams';
const logoLines = termWidth >= 95 ? logoWide : logoNarrow;
const logo = logoLines.map((line) => color.blue(line)).join('\n');
const tagline = color.white(' Build More, Architect Dreams\n © BMad Code');
await prompts.box(`${logo}\n${tagline}`, `v${version}`, {
await prompts.box(`${logo}\n${tagline}`, '', {
contentAlign: 'center',
rounded: true,
formatBorder: color.blue,

View File

@ -26,6 +26,44 @@ class Installer {
this.bmadFolderName = BMAD_FOLDER_NAME;
}
/**
* Read the module version from .claude-plugin/marketplace.json
* Walks up from sourcePath looking for .claude-plugin/marketplace.json
* @param {string} sourcePath - Module source directory
* @returns {string} Version string or empty string
*/
async _getMarketplaceVersion(sourcePath) {
let dir = sourcePath;
for (let i = 0; i < 5; i++) {
const marketplacePath = path.join(dir, '.claude-plugin', 'marketplace.json');
if (await fs.pathExists(marketplacePath)) {
try {
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
return this._extractMarketplaceVersion(data);
} catch {
return '';
}
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return '';
}
/**
* Extract the highest version from marketplace.json plugins array
*/
_extractMarketplaceVersion(data) {
const plugins = data?.plugins;
if (!Array.isArray(plugins) || plugins.length === 0) return '';
let best = '';
for (const p of plugins) {
if (p.version && (!best || p.version > best)) best = p.version;
}
return best;
}
/**
* Main installation method
* @param {Object} config - Installation configuration
@ -52,9 +90,36 @@ class Installer {
await this._validateIdeSelection(config);
// Capture pre-install module versions for from→to display
const preInstallVersions = new Map();
if (existingInstall.installed) {
const existingModules = await this.manifest.getAllModuleVersions(paths.bmadDir);
for (const mod of existingModules) {
if (mod.name && mod.version) {
preInstallVersions.set(mod.name, mod.version);
}
}
}
// Results collector for consolidated summary
const results = [];
const addResult = (step, status, detail = '') => results.push({ step, status, detail });
const addResult = (step, status, detail = '', meta = {}) => results.push({ step, status, detail, ...meta });
// Capture previously installed skill IDs before they get overwritten
const previousSkillIds = new Set();
const prevCsvPath = path.join(paths.bmadDir, '_config', 'skill-manifest.csv');
if (await fs.pathExists(prevCsvPath)) {
try {
const csvParse = require('csv-parse/sync');
const content = await fs.readFile(prevCsvPath, 'utf8');
const records = csvParse.parse(content, { columns: true, skip_empty_lines: true });
for (const r of records) {
if (r.canonicalId) previousSkillIds.add(r.canonicalId);
}
} catch (error) {
await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
}
}
await this._cacheCustomModules(paths, addResult);
@ -65,7 +130,7 @@ class Installer {
await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules);
await this._setupIdes(config, allModules, paths, addResult);
await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
const restoreResult = await this._restoreUserFiles(paths, updateState);
@ -76,6 +141,7 @@ class Installer {
ides: config.ides,
customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
preInstallVersions,
});
return {
@ -321,7 +387,7 @@ class Installer {
/**
* Set up IDE integrations for each selected IDE.
*/
async _setupIdes(config, allModules, paths, addResult) {
async _setupIdes(config, allModules, paths, addResult, previousSkillIds = new Set()) {
if (config.skipIde || !config.ides || config.ides.length === 0) return;
await this.ideManager.ensureInitialized();
@ -336,6 +402,7 @@ class Installer {
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
selectedModules: allModules || [],
verbose: config.verbose,
previousSkillIds,
});
if (setupResult.success) {
@ -556,7 +623,7 @@ class Installer {
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
await officialModules.install(
const installResult = await officialModules.install(
moduleName,
paths.bmadDir,
(filePath) => {
@ -570,7 +637,12 @@ class Installer {
},
);
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
// Get display name from source module.yaml; version from marketplace.json
const sourcePath = await officialModules.findModuleSource(moduleName, { silent: true });
const moduleInfo = sourcePath ? await officialModules.getModuleInfo(sourcePath, moduleName, '') : null;
const displayName = moduleInfo?.name || moduleName;
const version = sourcePath ? await this._getMarketplaceVersion(sourcePath) : '';
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
}
}
@ -598,7 +670,11 @@ class Installer {
[moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
});
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
// Get display name from source module.yaml; version from marketplace.json
const moduleInfo = await officialModules.getModuleInfo(sourcePath, moduleName, '');
const displayName = moduleInfo?.name || moduleName;
const version = await this._getMarketplaceVersion(sourcePath);
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
}
}
@ -1062,23 +1138,10 @@ class Installer {
const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
// Build step lines with status indicators
const preVersions = context.preInstallVersions || new Map();
const lines = [];
for (const r of results) {
let stepLabel = null;
if (r.status !== 'ok') {
stepLabel = r.step;
} else if (r.step === 'Core') {
stepLabel = 'BMAD';
} else if (r.step.startsWith('Module: ')) {
stepLabel = r.step;
} else if (selectedIdes.has(String(r.step).toLowerCase())) {
stepLabel = r.step;
}
if (!stepLabel) {
continue;
}
const stepLabel = r.step;
let icon;
if (r.status === 'ok') {
@ -1088,18 +1151,32 @@ class Installer {
} else {
icon = color.red('\u2717');
}
const detail = r.detail ? color.dim(` (${r.detail})`) : '';
// Build version detail for module results
let detail = '';
if (r.moduleCode && r.newVersion) {
const oldVersion = preVersions.get(r.moduleCode);
if (oldVersion && oldVersion === r.newVersion) {
detail = ` (v${r.newVersion}, no change)`;
} else if (oldVersion) {
detail = ` (v${oldVersion} → v${r.newVersion})`;
} else {
detail = ` (v${r.newVersion}, installed)`;
}
} else if (r.detail) {
detail = ` (${r.detail})`;
}
lines.push(` ${icon} ${stepLabel}${detail}`);
}
if ((context.ides || []).length === 0) {
lines.push(` ${color.green('\u2713')} No IDE selected ${color.dim('(installed in _bmad only)')}`);
lines.push(` ${color.green('\u2713')} No IDE selected (installed in _bmad only)`);
}
// Context and warnings
lines.push('');
if (context.bmadDir) {
lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
lines.push(` Installed to: ${context.bmadDir}`);
}
if (context.customFiles && context.customFiles.length > 0) {
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
@ -1111,17 +1188,18 @@ class Installer {
// Next steps
lines.push(
'',
' Next steps:',
` Read our new Docs Site: ${color.dim('https://docs.bmad-method.org/')}`,
` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`,
` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`,
' Get started:',
` 1. Launch your AI agent from your project folder`,
` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
'',
` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
);
if (context.ides && context.ides.length > 0) {
lines.push(` Invoke the ${color.cyan('bmad-help')} skill in your IDE Agent to get started`);
}
await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
await prompts.box(lines.join('\n'), 'BMAD is ready to use!', {
rounded: true,
formatBorder: color.green,
});
}
/**
@ -1231,6 +1309,7 @@ class Installer {
}
for (const moduleName of modulesToUpdate) {
if (moduleName === 'core') continue; // Already collected above
const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
if (modulePrompted) {
promptedForNewFields = true;

View File

@ -837,14 +837,13 @@ class Manifest {
* @returns {Object} Version info object with version, source, npmPackage, repoUrl
*/
async getModuleVersionInfo(moduleName, bmadDir, moduleSourcePath = null) {
const os = require('node:os');
const yaml = require('yaml');
// Built-in modules use BMad version (only core and bmm are in BMAD-METHOD repo)
// Resolve source type first, then read version with the correct path context
if (['core', 'bmm'].includes(moduleName)) {
const bmadVersion = require(path.join(getProjectRoot(), 'package.json')).version;
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
return {
version: bmadVersion,
version,
source: 'built-in',
npmPackage: null,
repoUrl: null,
@ -857,42 +856,20 @@ class Manifest {
const moduleInfo = await extMgr.getModuleByCode(moduleName);
if (moduleInfo) {
// External module - try to get version from npm registry first, then fall back to cache
let version = null;
if (moduleInfo.npmPackage) {
// Fetch version from npm registry
try {
version = await this.fetchNpmVersion(moduleInfo.npmPackage);
} catch {
// npm fetch failed, try cache as fallback
}
}
// If npm didn't work, try reading from cached repo's package.json
if (!version) {
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
const packageJsonPath = path.join(cacheDir, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
try {
const pkg = require(packageJsonPath);
version = pkg.version;
} catch (error) {
await prompts.log.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
}
}
}
// External module: use moduleSourcePath if provided, otherwise fall back to cache
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
return {
version: version,
version,
source: 'external',
npmPackage: moduleInfo.npmPackage || null,
repoUrl: moduleInfo.url || null,
};
}
// Custom module - check cache directory
// Custom module: resolve path from source or cache before reading version
const customSourcePath = moduleSourcePath || path.join(bmadDir, '_config', 'custom', moduleName);
const version = await this._readMarketplaceVersion(moduleName, customSourcePath);
const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName);
const moduleYamlPath = path.join(cacheDir, 'module.yaml');
@ -901,7 +878,7 @@ class Manifest {
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
const moduleConfig = yaml.parse(yamlContent);
return {
version: moduleConfig.version || null,
version: version || moduleConfig.version || null,
source: 'custom',
npmPackage: moduleConfig.npmPackage || null,
repoUrl: moduleConfig.repoUrl || null,
@ -913,13 +890,62 @@ class Manifest {
// Unknown module
return {
version: null,
version,
source: 'unknown',
npmPackage: null,
repoUrl: null,
};
}
/**
* Read version from .claude-plugin/marketplace.json for a module
* @param {string} moduleName - Module code
* @returns {string|null} Version or null
*/
async _readMarketplaceVersion(moduleName, moduleSourcePath = null) {
const os = require('node:os');
let marketplacePath;
if (['core', 'bmm'].includes(moduleName)) {
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
} else if (moduleSourcePath) {
// Walk up from source path to find marketplace.json
let dir = moduleSourcePath;
for (let i = 0; i < 5; i++) {
const candidate = path.join(dir, '.claude-plugin', 'marketplace.json');
if (await fs.pathExists(candidate)) {
marketplacePath = candidate;
break;
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
}
// Fallback to external module cache
if (!marketplacePath) {
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
}
try {
if (await fs.pathExists(marketplacePath)) {
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
const plugins = data?.plugins;
if (!Array.isArray(plugins) || plugins.length === 0) return null;
let best = null;
for (const p of plugins) {
if (p.version && (!best || p.version > best)) best = p.version;
}
return best;
}
} catch {
// ignore
}
return null;
}
/**
* Fetch latest version from npm for a package
* @param {string} packageName - npm package name

View File

@ -86,7 +86,7 @@ class ConfigDrivenIdeSetup {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
// Clean up any old BMAD installation first
await this.cleanup(projectDir, options);
await this.cleanup(projectDir, options, bmadDir);
if (!this.installerConfig) {
return { success: false, reason: 'no-config' };
@ -215,15 +215,34 @@ class ConfigDrivenIdeSetup {
* Cleanup IDE configuration
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir, options = {}) {
async cleanup(projectDir, options = {}, bmadDir = null) {
const resolvedBmadDir = bmadDir || (await this._findBmadDir(projectDir));
// Build removal set: previously installed skills + removals.txt entries
let removalSet;
if (options.previousSkillIds && options.previousSkillIds.size > 0) {
// Install/update flow: use pre-captured skill IDs (before manifest was overwritten)
removalSet = new Set(options.previousSkillIds);
if (resolvedBmadDir) {
const removals = await this.loadRemovalLists(resolvedBmadDir);
for (const entry of removals) removalSet.add(entry);
}
} else if (resolvedBmadDir) {
// Uninstall flow: read from current skill-manifest.csv + removals.txt
removalSet = await this._buildUninstallSet(resolvedBmadDir);
} else {
removalSet = new Set();
}
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
// Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
if (this.installerConfig?.legacy_targets) {
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
for (const legacyDir of this.installerConfig.legacy_targets) {
if (this.isGlobalPath(legacyDir)) {
await this.warnGlobalLegacy(legacyDir, options);
} else {
await this.cleanupTarget(projectDir, legacyDir, options);
await this.cleanupTarget(projectDir, legacyDir, options, null);
await this.removeEmptyParents(projectDir, legacyDir);
}
}
@ -244,9 +263,9 @@ class ConfigDrivenIdeSetup {
await this.cleanupRovoDevPrompts(projectDir, options);
}
// Clean target directory
// Clean current target directory
if (this.installerConfig?.target_dir) {
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options);
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet);
}
}
@ -286,23 +305,117 @@ class ConfigDrivenIdeSetup {
}
/**
* Cleanup a specific target directory
* Find the _bmad directory in a project
* @param {string} projectDir - Project directory
* @returns {string|null} Path to bmad dir or null
*/
async _findBmadDir(projectDir) {
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
return (await fs.pathExists(bmadDir)) ? bmadDir : null;
}
/**
* Build the full set of entries to remove for uninstall.
* Reads skill-manifest.csv to know exactly what was installed, plus removal lists.
* @param {string} bmadDir - BMAD installation directory
* @returns {Set<string>} Set of entries to remove
*/
async _buildUninstallSet(bmadDir) {
const removals = await this.loadRemovalLists(bmadDir);
// Also add all currently installed skills from skill-manifest.csv
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
try {
if (await fs.pathExists(csvPath)) {
const content = await fs.readFile(csvPath, 'utf8');
const records = csv.parse(content, { columns: true, skip_empty_lines: true });
for (const record of records) {
if (record.canonicalId) {
removals.add(record.canonicalId);
}
}
}
} catch {
// If we can't read the manifest, we still have the removal lists
}
return removals;
}
/**
* Load removal lists from all module sources in the bmad directory.
* Each module can have an optional removals.txt listing entries to remove.
* @param {string} bmadDir - BMAD installation directory
* @returns {Set<string>} Set of entries to remove
*/
async loadRemovalLists(bmadDir) {
const removals = new Set();
const { getProjectRoot } = require('../project-root');
// Read project-level removals.txt (covers core and bmm)
const projectRemovalsPath = path.join(getProjectRoot(), 'removals.txt');
await this._readRemovalFile(projectRemovalsPath, removals);
// Read per-module removals.txt from installed module directories
try {
const entries = await fs.readdir(bmadDir);
for (const entry of entries) {
if (entry.startsWith('_')) continue;
const removalPath = path.join(bmadDir, entry, 'removals.txt');
await this._readRemovalFile(removalPath, removals);
}
} catch {
// bmadDir may not exist yet on fresh install
}
return removals;
}
/**
* Read a removals.txt file and add entries to the set
* @param {string} filePath - Path to removals.txt
* @param {Set<string>} removals - Set to add entries to
*/
async _readRemovalFile(filePath, removals) {
try {
if (await fs.pathExists(filePath)) {
const content = await fs.readFile(filePath, 'utf8');
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
removals.add(trimmed);
}
}
}
} catch {
// Optional file — ignore errors
}
}
/**
* Cleanup a specific target directory.
* When removalSet is provided, only removes entries in that set.
* When removalSet is null (legacy dirs), removes all bmad-prefixed entries.
* @param {string} projectDir - Project directory
* @param {string} targetDir - Target directory to clean
* @param {Object} options - Cleanup options
* @param {Set<string>|null} removalSet - Entries to remove, or null for legacy prefix matching
*/
async cleanupTarget(projectDir, targetDir, options = {}) {
async cleanupTarget(projectDir, targetDir, options = {}, removalSet = new Set()) {
const targetPath = path.join(projectDir, targetDir);
if (!(await fs.pathExists(targetPath))) {
return;
}
// Remove all bmad* files
if (removalSet && removalSet.size === 0) {
return;
}
let entries;
try {
entries = await fs.readdir(targetPath);
} catch {
// Directory exists but can't be read - skip cleanup
return;
}
@ -313,23 +426,26 @@ class ConfigDrivenIdeSetup {
let removedCount = 0;
for (const entry of entries) {
if (!entry || typeof entry !== 'string') {
continue;
}
if (entry.startsWith('bmad') && !entry.startsWith('bmad-os-')) {
const entryPath = path.join(targetPath, entry);
if (!entry || typeof entry !== 'string') continue;
// Always preserve bmad-os-* utility skills regardless of cleanup mode
if (entry.startsWith('bmad-os-')) continue;
// Surgical removal from set, or legacy prefix matching when set is null
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
if (shouldRemove) {
try {
await fs.remove(entryPath);
await fs.remove(path.join(targetPath, entry));
removedCount++;
} catch {
// Skip entries that can't be removed (broken symlinks, permission errors)
// Skip entries that can't be removed
}
}
}
if (removedCount > 0 && !options.silent) {
await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`);
}
// Only log cleanup when it's not a routine reinstall (legacy dir cleanup or actual removals)
// Suppress for current target_dir since it's always cleaned before a fresh write
// Remove empty directory after cleanup
if (removedCount > 0) {
@ -339,7 +455,7 @@ class ConfigDrivenIdeSetup {
await fs.remove(targetPath);
}
} catch {
// Directory may already be gone or in use — skip
// Directory may already be gone or in use
}
}
}

View File

@ -6,32 +6,25 @@
startMessage: |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎉 V6 IS HERE! Welcome to BMad Method V6 - Official Stable Release!
Agile AI-Driven Development. Powered by BMad Core and a growing module ecosystem.
Install official and community modules during setup to customize your experience.
The BMad Method is now a Platform powered by the BMad Method Core and Module Ecosystem!
- Select and install modules during setup - customize your experience
- New BMad Method for Agile AI-Driven Development (the evolution of V4)
- Exciting new modules available during installation, with community modules coming soon
- Documentation: https://docs.bmad-method.org
🌟 100% free. 100% open source. Always.
No paywalls. No gated content. Knowledge shared, not sold.
🌟 BMad is 100% free and open source.
- No gated Discord. No paywalls. No gated content.
- We believe in empowering everyone, not just those who can pay.
- Knowledge should be shared, not sold.
🌐 CONNECT:
Website: https://bmadcode.com/
Discord: https://discord.gg/gk8jAdXWmj
YouTube: https://www.youtube.com/@BMadCode
X: https://x.com/BMadCode
Facebook: https://facebook.com/@BMadCode
🎤 SPEAKING & MEDIA:
- Available for conferences, podcasts, and media appearances
- Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
- For speaking inquiries or interviews, reach out to BMad on Discord!
⭐ SUPPORT THE PROJECT:
Star us: https://github.com/bmad-code-org/BMAD-METHOD/
Donate: https://buymeacoffee.com/bmad
Corporate sponsorship and speaking inquiries: contact@bmadcode.com
⭐ HELP US GROW:
- Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
- Subscribe on YouTube: https://www.youtube.com/@BMadCode
- Free Community and Support: https://discord.gg/gk8jAdXWmj
- Donate: https://buymeacoffee.com/bmad
- Corporate Sponsorship available
Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/blob/main/CHANGELOG.md
Docs, blog, and latest updates: https://bmadcode.com/
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

View File

@ -4,8 +4,50 @@ const fs = require('fs-extra');
const { CLIUtils } = require('./cli-utils');
const { CustomHandler } = require('./custom-handler');
const { ExternalModuleManager } = require('./modules/external-manager');
const { getProjectRoot } = require('./project-root');
const prompts = require('./prompts');
/**
* Read module version from .claude-plugin/marketplace.json
* @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis')
* @returns {string} Version string or empty string
*/
async function getMarketplaceVersion(moduleCode) {
let marketplacePath;
if (moduleCode === 'core' || moduleCode === 'bmm') {
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
} else {
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleCode);
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
}
try {
if (await fs.pathExists(marketplacePath)) {
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
return _extractMarketplaceVersion(data);
}
} catch {
// ignore
}
return '';
}
/**
* Extract the highest version from marketplace.json plugins array.
* Handles multiple plugins per file safely.
* @param {Object} data - Parsed marketplace.json
* @returns {string} Version string or empty string
*/
function _extractMarketplaceVersion(data) {
const plugins = data?.plugins;
if (!Array.isArray(plugins) || plugins.length === 0) return '';
// Use the highest version across all plugins in the file
let best = '';
for (const p of plugins) {
if (p.version && (!best || p.version > best)) best = p.version;
}
return best;
}
// Separator class for visual grouping in select/multiselect prompts
// Note: @clack/prompts doesn't support separators natively, they are filtered out
class Separator {
@ -70,17 +112,14 @@ class UI {
if (hasExistingInstall) {
// Get version information
const { existingInstall, bmadDir } = await this.getExistingInstallation(confirmedDirectory);
const packageJsonPath = path.join(__dirname, '../../package.json');
const currentVersion = require(packageJsonPath).version;
const installedVersion = existingInstall.installed ? existingInstall.version || 'unknown' : 'unknown';
// Build menu choices dynamically
const choices = [];
// Always show Quick Update first (allows refreshing installation even on same version)
if (installedVersion !== 'unknown') {
if (existingInstall.installed) {
choices.push({
name: `Quick Update (v${installedVersion} → v${currentVersion})`,
name: 'Quick Update',
value: 'quick-update',
});
}
@ -880,14 +919,18 @@ class UI {
const lockedValues = ['core'];
// Core module is always installed — show it locked at the top
allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' });
const coreVersion = await getMarketplaceVersion('core');
const coreLabel = coreVersion ? `BMad Core Module (v${coreVersion})` : 'BMad Core Module';
allOptions.push({ label: coreLabel, value: 'core', hint: 'Core configuration and shared resources' });
initialValues.push('core');
// Helper to build module entry with proper sorting and selection
const buildModuleEntry = (mod, value, group) => {
const buildModuleEntry = async (mod, value, group) => {
const isInstalled = installedModuleIds.has(value);
const version = await getMarketplaceVersion(value);
const label = version ? `${mod.name} (v${version})` : mod.name;
return {
label: mod.name,
label,
value,
hint: mod.description || group,
// Pre-select only if already installed (not on fresh install)
@ -899,7 +942,7 @@ class UI {
const localEntries = [];
for (const mod of localModules) {
if (!mod.isCustom && mod.id !== 'core') {
const entry = buildModuleEntry(mod, mod.id, 'Local');
const entry = await buildModuleEntry(mod, mod.id, 'Local');
localEntries.push(entry);
if (entry.selected) {
initialValues.push(mod.id);
@ -912,7 +955,7 @@ class UI {
const officialModules = [];
for (const mod of externalModules) {
if (mod.type === 'bmad-org') {
const entry = buildModuleEntry(mod, mod.code, 'Official');
const entry = await buildModuleEntry(mod, mod.code, 'Official');
officialModules.push(entry);
if (entry.selected) {
initialValues.push(mod.code);
@ -925,7 +968,7 @@ class UI {
const communityModules = [];
for (const mod of externalModules) {
if (mod.type === 'community') {
const entry = buildModuleEntry(mod, mod.code, 'Community');
const entry = await buildModuleEntry(mod, mod.code, 'Community');
communityModules.push(entry);
if (entry.selected) {
initialValues.push(mod.code);