Skip to content

Commit c46502f

Browse files
authored
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.
1 parent 4799153 commit c46502f

8 files changed

Lines changed: 425 additions & 134 deletions

File tree

removals.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# BMad Method - Skill Removal List
2+
# Entries listed here will be removed from IDE skill directories during install/update.
3+
# One entry per line. Lines starting with # are comments.
4+
# Each entry is a skill directory name (canonicalId) that was removed or renamed.
5+
6+
# Removed agents (v6.2.0 - v6.2.2)
7+
bmad-agent-sm
8+
bmad-agent-qa
9+
bmad-agent-quick-flow-solo-dev
10+
11+
# Removed skills (v6.2.0 - v6.2.2)
12+
bmad-create-product-brief
13+
bmad-product-brief-preview
14+
bmad-quick-spec
15+
bmad-quick-flow
16+
bmad-quick-dev-new-preview
17+
bmad-init

test/test-installation-components.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,14 @@ async function runTests() {
13011301
'---\nname: bmad-architect\ndescription: Architect\n---\nOld skill content\n',
13021302
);
13031303

1304+
// Add bmad-architect to the existing skill-manifest.csv so cleanup knows it was previously installed
1305+
const configDir27 = path.join(installedBmadDir27, '_config');
1306+
const existingCsv27 = await fs.readFile(path.join(configDir27, 'skill-manifest.csv'), 'utf8');
1307+
await fs.writeFile(
1308+
path.join(configDir27, 'skill-manifest.csv'),
1309+
existingCsv27.trimEnd() + '\n"bmad-architect","bmad-architect","Architect","bmm","_bmad/bmm/agents/bmad-architect/SKILL.md","true"\n',
1310+
);
1311+
13041312
// Run Claude Code setup (which triggers cleanup then install)
13051313
const ideManager27 = new IdeManager();
13061314
await ideManager27.ensureInitialized();

tools/installer/cli-utils.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,33 @@ const CLIUtils = {
1919
* Display BMAD logo and version using @clack intro + box
2020
*/
2121
async displayLogo() {
22-
const version = this.getVersion();
2322
const color = await prompts.getColor();
24-
25-
// ASCII art logo
26-
const logo = [
23+
const termWidth = process.stdout.columns || 80;
24+
25+
// Full "BMad Method" logo for wide terminals, "BMad" only for narrow
26+
const logoWide = [
27+
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ™',
28+
'██╔══██╗████╗ ████║██╔══██╗██╔══██╗ ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗',
29+
'██████╔╝██╔████╔██║███████║██║ ██║ ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║',
30+
'██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║ ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║',
31+
'██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝',
32+
'╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ',
33+
];
34+
35+
const logoNarrow = [
2736
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
2837
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
2938
' ██████╔╝██╔████╔██║███████║██║ ██║',
3039
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
3140
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
3241
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
33-
]
34-
.map((line) => color.yellow(line))
35-
.join('\n');
42+
];
3643

37-
const tagline = ' Build More, Architect Dreams';
44+
const logoLines = termWidth >= 95 ? logoWide : logoNarrow;
45+
const logo = logoLines.map((line) => color.blue(line)).join('\n');
46+
const tagline = color.white(' Build More, Architect Dreams\n © BMad Code');
3847

39-
await prompts.box(`${logo}\n${tagline}`, `v${version}`, {
48+
await prompts.box(`${logo}\n${tagline}`, '', {
4049
contentAlign: 'center',
4150
rounded: true,
4251
formatBorder: color.blue,

tools/installer/core/installer.js

Lines changed: 112 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,44 @@ class Installer {
2626
this.bmadFolderName = BMAD_FOLDER_NAME;
2727
}
2828

29+
/**
30+
* Read the module version from .claude-plugin/marketplace.json
31+
* Walks up from sourcePath looking for .claude-plugin/marketplace.json
32+
* @param {string} sourcePath - Module source directory
33+
* @returns {string} Version string or empty string
34+
*/
35+
async _getMarketplaceVersion(sourcePath) {
36+
let dir = sourcePath;
37+
for (let i = 0; i < 5; i++) {
38+
const marketplacePath = path.join(dir, '.claude-plugin', 'marketplace.json');
39+
if (await fs.pathExists(marketplacePath)) {
40+
try {
41+
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
42+
return this._extractMarketplaceVersion(data);
43+
} catch {
44+
return '';
45+
}
46+
}
47+
const parent = path.dirname(dir);
48+
if (parent === dir) break;
49+
dir = parent;
50+
}
51+
return '';
52+
}
53+
54+
/**
55+
* Extract the highest version from marketplace.json plugins array
56+
*/
57+
_extractMarketplaceVersion(data) {
58+
const plugins = data?.plugins;
59+
if (!Array.isArray(plugins) || plugins.length === 0) return '';
60+
let best = '';
61+
for (const p of plugins) {
62+
if (p.version && (!best || p.version > best)) best = p.version;
63+
}
64+
return best;
65+
}
66+
2967
/**
3068
* Main installation method
3169
* @param {Object} config - Installation configuration
@@ -52,9 +90,36 @@ class Installer {
5290

5391
await this._validateIdeSelection(config);
5492

93+
// Capture pre-install module versions for from→to display
94+
const preInstallVersions = new Map();
95+
if (existingInstall.installed) {
96+
const existingModules = await this.manifest.getAllModuleVersions(paths.bmadDir);
97+
for (const mod of existingModules) {
98+
if (mod.name && mod.version) {
99+
preInstallVersions.set(mod.name, mod.version);
100+
}
101+
}
102+
}
103+
55104
// Results collector for consolidated summary
56105
const results = [];
57-
const addResult = (step, status, detail = '') => results.push({ step, status, detail });
106+
const addResult = (step, status, detail = '', meta = {}) => results.push({ step, status, detail, ...meta });
107+
108+
// Capture previously installed skill IDs before they get overwritten
109+
const previousSkillIds = new Set();
110+
const prevCsvPath = path.join(paths.bmadDir, '_config', 'skill-manifest.csv');
111+
if (await fs.pathExists(prevCsvPath)) {
112+
try {
113+
const csvParse = require('csv-parse/sync');
114+
const content = await fs.readFile(prevCsvPath, 'utf8');
115+
const records = csvParse.parse(content, { columns: true, skip_empty_lines: true });
116+
for (const r of records) {
117+
if (r.canonicalId) previousSkillIds.add(r.canonicalId);
118+
}
119+
} catch (error) {
120+
await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
121+
}
122+
}
58123

59124
await this._cacheCustomModules(paths, addResult);
60125

@@ -65,7 +130,7 @@ class Installer {
65130

66131
await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules);
67132

68-
await this._setupIdes(config, allModules, paths, addResult);
133+
await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
69134

70135
const restoreResult = await this._restoreUserFiles(paths, updateState);
71136

@@ -76,6 +141,7 @@ class Installer {
76141
ides: config.ides,
77142
customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
78143
modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
144+
preInstallVersions,
79145
});
80146

81147
return {
@@ -321,7 +387,7 @@ class Installer {
321387
/**
322388
* Set up IDE integrations for each selected IDE.
323389
*/
324-
async _setupIdes(config, allModules, paths, addResult) {
390+
async _setupIdes(config, allModules, paths, addResult, previousSkillIds = new Set()) {
325391
if (config.skipIde || !config.ides || config.ides.length === 0) return;
326392

327393
await this.ideManager.ensureInitialized();
@@ -336,6 +402,7 @@ class Installer {
336402
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
337403
selectedModules: allModules || [],
338404
verbose: config.verbose,
405+
previousSkillIds,
339406
});
340407

341408
if (setupResult.success) {
@@ -556,7 +623,7 @@ class Installer {
556623
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
557624

558625
const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
559-
await officialModules.install(
626+
const installResult = await officialModules.install(
560627
moduleName,
561628
paths.bmadDir,
562629
(filePath) => {
@@ -570,7 +637,12 @@ class Installer {
570637
},
571638
);
572639

573-
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
640+
// Get display name from source module.yaml; version from marketplace.json
641+
const sourcePath = await officialModules.findModuleSource(moduleName, { silent: true });
642+
const moduleInfo = sourcePath ? await officialModules.getModuleInfo(sourcePath, moduleName, '') : null;
643+
const displayName = moduleInfo?.name || moduleName;
644+
const version = sourcePath ? await this._getMarketplaceVersion(sourcePath) : '';
645+
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
574646
}
575647
}
576648

@@ -598,7 +670,11 @@ class Installer {
598670
[moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
599671
});
600672

601-
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
673+
// Get display name from source module.yaml; version from marketplace.json
674+
const moduleInfo = await officialModules.getModuleInfo(sourcePath, moduleName, '');
675+
const displayName = moduleInfo?.name || moduleName;
676+
const version = await this._getMarketplaceVersion(sourcePath);
677+
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
602678
}
603679
}
604680

@@ -1062,23 +1138,10 @@ class Installer {
10621138
const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
10631139

10641140
// Build step lines with status indicators
1141+
const preVersions = context.preInstallVersions || new Map();
10651142
const lines = [];
10661143
for (const r of results) {
1067-
let stepLabel = null;
1068-
1069-
if (r.status !== 'ok') {
1070-
stepLabel = r.step;
1071-
} else if (r.step === 'Core') {
1072-
stepLabel = 'BMAD';
1073-
} else if (r.step.startsWith('Module: ')) {
1074-
stepLabel = r.step;
1075-
} else if (selectedIdes.has(String(r.step).toLowerCase())) {
1076-
stepLabel = r.step;
1077-
}
1078-
1079-
if (!stepLabel) {
1080-
continue;
1081-
}
1144+
const stepLabel = r.step;
10821145

10831146
let icon;
10841147
if (r.status === 'ok') {
@@ -1088,18 +1151,32 @@ class Installer {
10881151
} else {
10891152
icon = color.red('\u2717');
10901153
}
1091-
const detail = r.detail ? color.dim(` (${r.detail})`) : '';
1154+
1155+
// Build version detail for module results
1156+
let detail = '';
1157+
if (r.moduleCode && r.newVersion) {
1158+
const oldVersion = preVersions.get(r.moduleCode);
1159+
if (oldVersion && oldVersion === r.newVersion) {
1160+
detail = ` (v${r.newVersion}, no change)`;
1161+
} else if (oldVersion) {
1162+
detail = ` (v${oldVersion} → v${r.newVersion})`;
1163+
} else {
1164+
detail = ` (v${r.newVersion}, installed)`;
1165+
}
1166+
} else if (r.detail) {
1167+
detail = ` (${r.detail})`;
1168+
}
10921169
lines.push(` ${icon} ${stepLabel}${detail}`);
10931170
}
10941171

10951172
if ((context.ides || []).length === 0) {
1096-
lines.push(` ${color.green('\u2713')} No IDE selected ${color.dim('(installed in _bmad only)')}`);
1173+
lines.push(` ${color.green('\u2713')} No IDE selected (installed in _bmad only)`);
10971174
}
10981175

10991176
// Context and warnings
11001177
lines.push('');
11011178
if (context.bmadDir) {
1102-
lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
1179+
lines.push(` Installed to: ${context.bmadDir}`);
11031180
}
11041181
if (context.customFiles && context.customFiles.length > 0) {
11051182
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
@@ -1111,17 +1188,18 @@ class Installer {
11111188
// Next steps
11121189
lines.push(
11131190
'',
1114-
' Next steps:',
1115-
` Read our new Docs Site: ${color.dim('https://docs.bmad-method.org/')}`,
1116-
` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
1117-
` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`,
1118-
` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`,
1191+
' Get started:',
1192+
` 1. Launch your AI agent from your project folder`,
1193+
` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
1194+
'',
1195+
` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
1196+
` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
11191197
);
1120-
if (context.ides && context.ides.length > 0) {
1121-
lines.push(` Invoke the ${color.cyan('bmad-help')} skill in your IDE Agent to get started`);
1122-
}
11231198

1124-
await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
1199+
await prompts.box(lines.join('\n'), 'BMAD is ready to use!', {
1200+
rounded: true,
1201+
formatBorder: color.green,
1202+
});
11251203
}
11261204

11271205
/**
@@ -1231,6 +1309,7 @@ class Installer {
12311309
}
12321310

12331311
for (const moduleName of modulesToUpdate) {
1312+
if (moduleName === 'core') continue; // Already collected above
12341313
const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
12351314
if (modulePrompted) {
12361315
promptedForNewFields = true;

0 commit comments

Comments
 (0)