bmad-method 6.2.3-next.22 → 6.2.3-next.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  [![Version](https://img.shields.io/npm/v/bmad-method?color=blue&label=version)](https://www.npmjs.com/package/bmad-method)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
5
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen)](https://nodejs.org)
6
+ [![Python Version](https://img.shields.io/badge/python-%3E%3D3.10-blue?logo=python&logoColor=white)](https://www.python.org)
7
+ [![uv](https://img.shields.io/badge/uv-package%20manager-blueviolet?logo=uv)](https://docs.astral.sh/uv/)
6
8
  [![Discord](https://img.shields.io/badge/Discord-Join%20Community-7289da?logo=discord&logoColor=white)](https://discord.gg/gk8jAdXWmj)
7
9
 
8
10
  **Build More Architect Dreams** — An AI-driven agile development module for the BMad Method Module Ecosystem, the best and most comprehensive Agile AI Driven Development framework that has true scale-adaptive intelligence that adjusts from bug fixes to enterprise systems.
@@ -34,7 +36,7 @@ Traditional AI tools do the thinking for you, producing average results. BMad ag
34
36
 
35
37
  ## Quick Start
36
38
 
37
- **Prerequisites**: [Node.js](https://nodejs.org) v20+
39
+ **Prerequisites**: [Node.js](https://nodejs.org) v20+ · [Python](https://www.python.org) 3.10+ · [uv](https://docs.astral.sh/uv/)
38
40
 
39
41
  ```bash
40
42
  npx bmad-method install
@@ -79,18 +81,15 @@ BMad Method extends with official modules for specialized domains. Available dur
79
81
  ## Community
80
82
 
81
83
  - [Discord](https://discord.gg/gk8jAdXWmj) — Get help, share ideas, collaborate
82
- - [Subscribe on YouTube](https://www.youtube.com/@BMadCode) — Tutorials, master class, and podcast (launching Feb 2025)
84
+ - [YouTube](https://youtube.com/@BMadCode) — Tutorials, master class, and more
85
+ - [X / Twitter](https://x.com/BMadCode)
86
+ - [Website](https://bmadcode.com)
83
87
  - [GitHub Issues](https://github.com/bmad-code-org/BMAD-METHOD/issues) — Bug reports and feature requests
84
88
  - [Discussions](https://github.com/bmad-code-org/BMAD-METHOD/discussions) — Community conversations
85
89
 
86
90
  ## Support BMad
87
91
 
88
- BMad is free for everyone and always will be. If you'd like to support development:
89
-
90
- - ⭐ Please click the star project icon near the top right of this page
91
- - ☕ [Buy Me a Coffee](https://buymeacoffee.com/bmad) — Fuel the development
92
- - 🏢 Corporate sponsorship — DM on Discord
93
- - 🎤 Speaking & Media — Available for conferences, podcasts, interviews (BM on Discord)
92
+ BMad is free for everyone and always will be. Star this repo, [buy me a coffee](https://buymeacoffee.com/bmad), or email <contact@bmadcode.com> for corporate sponsorship.
94
93
 
95
94
  ## Contributing
96
95
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.2.3-next.22",
4
+ "version": "6.2.3-next.23",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
package/removals.txt ADDED
@@ -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
@@ -19,24 +19,33 @@ const CLIUtils = {
19
19
  * Display BMAD logo and version using @clack intro + box
20
20
  */
21
21
  async displayLogo() {
22
- const version = this.getVersion();
23
22
  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 = [
27
36
  ' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
28
37
  ' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
29
38
  ' ██████╔╝██╔████╔██║███████║██║ ██║',
30
39
  ' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
31
40
  ' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
32
41
  ' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
33
- ]
34
- .map((line) => color.yellow(line))
35
- .join('\n');
42
+ ];
36
43
 
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');
38
47
 
39
- await prompts.box(`${logo}\n${tagline}`, `v${version}`, {
48
+ await prompts.box(`${logo}\n${tagline}`, '', {
40
49
  contentAlign: 'center',
41
50
  rounded: true,
42
51
  formatBorder: color.blue,
@@ -26,6 +26,44 @@ class Installer {
26
26
  this.bmadFolderName = BMAD_FOLDER_NAME;
27
27
  }
28
28
 
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
+
29
67
  /**
30
68
  * Main installation method
31
69
  * @param {Object} config - Installation configuration
@@ -52,9 +90,36 @@ class Installer {
52
90
 
53
91
  await this._validateIdeSelection(config);
54
92
 
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
+
55
104
  // Results collector for consolidated summary
56
105
  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
+ }
58
123
 
59
124
  await this._cacheCustomModules(paths, addResult);
60
125
 
@@ -65,7 +130,7 @@ class Installer {
65
130
 
66
131
  await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules);
67
132
 
68
- await this._setupIdes(config, allModules, paths, addResult);
133
+ await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
69
134
 
70
135
  const restoreResult = await this._restoreUserFiles(paths, updateState);
71
136
 
@@ -76,6 +141,7 @@ class Installer {
76
141
  ides: config.ides,
77
142
  customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
78
143
  modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
144
+ preInstallVersions,
79
145
  });
80
146
 
81
147
  return {
@@ -321,7 +387,7 @@ class Installer {
321
387
  /**
322
388
  * Set up IDE integrations for each selected IDE.
323
389
  */
324
- async _setupIdes(config, allModules, paths, addResult) {
390
+ async _setupIdes(config, allModules, paths, addResult, previousSkillIds = new Set()) {
325
391
  if (config.skipIde || !config.ides || config.ides.length === 0) return;
326
392
 
327
393
  await this.ideManager.ensureInitialized();
@@ -336,6 +402,7 @@ class Installer {
336
402
  const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
337
403
  selectedModules: allModules || [],
338
404
  verbose: config.verbose,
405
+ previousSkillIds,
339
406
  });
340
407
 
341
408
  if (setupResult.success) {
@@ -556,7 +623,7 @@ class Installer {
556
623
  message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
557
624
 
558
625
  const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
559
- await officialModules.install(
626
+ const installResult = await officialModules.install(
560
627
  moduleName,
561
628
  paths.bmadDir,
562
629
  (filePath) => {
@@ -570,7 +637,12 @@ class Installer {
570
637
  },
571
638
  );
572
639
 
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 });
574
646
  }
575
647
  }
576
648
 
@@ -598,7 +670,11 @@ class Installer {
598
670
  [moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
599
671
  });
600
672
 
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 });
602
678
  }
603
679
  }
604
680
 
@@ -1062,23 +1138,10 @@ class Installer {
1062
1138
  const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
1063
1139
 
1064
1140
  // Build step lines with status indicators
1141
+ const preVersions = context.preInstallVersions || new Map();
1065
1142
  const lines = [];
1066
1143
  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;
1082
1145
 
1083
1146
  let icon;
1084
1147
  if (r.status === 'ok') {
@@ -1088,18 +1151,32 @@ class Installer {
1088
1151
  } else {
1089
1152
  icon = color.red('\u2717');
1090
1153
  }
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
+ }
1092
1169
  lines.push(` ${icon} ${stepLabel}${detail}`);
1093
1170
  }
1094
1171
 
1095
1172
  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)`);
1097
1174
  }
1098
1175
 
1099
1176
  // Context and warnings
1100
1177
  lines.push('');
1101
1178
  if (context.bmadDir) {
1102
- lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
1179
+ lines.push(` Installed to: ${context.bmadDir}`);
1103
1180
  }
1104
1181
  if (context.customFiles && context.customFiles.length > 0) {
1105
1182
  lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
@@ -1111,17 +1188,18 @@ class Installer {
1111
1188
  // Next steps
1112
1189
  lines.push(
1113
1190
  '',
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')}`,
1119
1197
  );
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
- }
1123
1198
 
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
+ });
1125
1203
  }
1126
1204
 
1127
1205
  /**
@@ -1231,6 +1309,7 @@ class Installer {
1231
1309
  }
1232
1310
 
1233
1311
  for (const moduleName of modulesToUpdate) {
1312
+ if (moduleName === 'core') continue; // Already collected above
1234
1313
  const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
1235
1314
  if (modulePrompted) {
1236
1315
  promptedForNewFields = true;
@@ -837,14 +837,13 @@ class Manifest {
837
837
  * @returns {Object} Version info object with version, source, npmPackage, repoUrl
838
838
  */
839
839
  async getModuleVersionInfo(moduleName, bmadDir, moduleSourcePath = null) {
840
- const os = require('node:os');
841
840
  const yaml = require('yaml');
842
841
 
843
- // Built-in modules use BMad version (only core and bmm are in BMAD-METHOD repo)
842
+ // Resolve source type first, then read version with the correct path context
844
843
  if (['core', 'bmm'].includes(moduleName)) {
845
- const bmadVersion = require(path.join(getProjectRoot(), 'package.json')).version;
844
+ const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
846
845
  return {
847
- version: bmadVersion,
846
+ version,
848
847
  source: 'built-in',
849
848
  npmPackage: null,
850
849
  repoUrl: null,
@@ -857,42 +856,20 @@ class Manifest {
857
856
  const moduleInfo = await extMgr.getModuleByCode(moduleName);
858
857
 
859
858
  if (moduleInfo) {
860
- // External module - try to get version from npm registry first, then fall back to cache
861
- let version = null;
862
-
863
- if (moduleInfo.npmPackage) {
864
- // Fetch version from npm registry
865
- try {
866
- version = await this.fetchNpmVersion(moduleInfo.npmPackage);
867
- } catch {
868
- // npm fetch failed, try cache as fallback
869
- }
870
- }
871
-
872
- // If npm didn't work, try reading from cached repo's package.json
873
- if (!version) {
874
- const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
875
- const packageJsonPath = path.join(cacheDir, 'package.json');
876
-
877
- if (await fs.pathExists(packageJsonPath)) {
878
- try {
879
- const pkg = require(packageJsonPath);
880
- version = pkg.version;
881
- } catch (error) {
882
- await prompts.log.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
883
- }
884
- }
885
- }
886
-
859
+ // External module: use moduleSourcePath if provided, otherwise fall back to cache
860
+ const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
887
861
  return {
888
- version: version,
862
+ version,
889
863
  source: 'external',
890
864
  npmPackage: moduleInfo.npmPackage || null,
891
865
  repoUrl: moduleInfo.url || null,
892
866
  };
893
867
  }
894
868
 
895
- // Custom module - check cache directory
869
+ // Custom module: resolve path from source or cache before reading version
870
+ const customSourcePath = moduleSourcePath || path.join(bmadDir, '_config', 'custom', moduleName);
871
+ const version = await this._readMarketplaceVersion(moduleName, customSourcePath);
872
+
896
873
  const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName);
897
874
  const moduleYamlPath = path.join(cacheDir, 'module.yaml');
898
875
 
@@ -901,7 +878,7 @@ class Manifest {
901
878
  const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
902
879
  const moduleConfig = yaml.parse(yamlContent);
903
880
  return {
904
- version: moduleConfig.version || null,
881
+ version: version || moduleConfig.version || null,
905
882
  source: 'custom',
906
883
  npmPackage: moduleConfig.npmPackage || null,
907
884
  repoUrl: moduleConfig.repoUrl || null,
@@ -913,13 +890,62 @@ class Manifest {
913
890
 
914
891
  // Unknown module
915
892
  return {
916
- version: null,
893
+ version,
917
894
  source: 'unknown',
918
895
  npmPackage: null,
919
896
  repoUrl: null,
920
897
  };
921
898
  }
922
899
 
900
+ /**
901
+ * Read version from .claude-plugin/marketplace.json for a module
902
+ * @param {string} moduleName - Module code
903
+ * @returns {string|null} Version or null
904
+ */
905
+ async _readMarketplaceVersion(moduleName, moduleSourcePath = null) {
906
+ const os = require('node:os');
907
+ let marketplacePath;
908
+
909
+ if (['core', 'bmm'].includes(moduleName)) {
910
+ marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
911
+ } else if (moduleSourcePath) {
912
+ // Walk up from source path to find marketplace.json
913
+ let dir = moduleSourcePath;
914
+ for (let i = 0; i < 5; i++) {
915
+ const candidate = path.join(dir, '.claude-plugin', 'marketplace.json');
916
+ if (await fs.pathExists(candidate)) {
917
+ marketplacePath = candidate;
918
+ break;
919
+ }
920
+ const parent = path.dirname(dir);
921
+ if (parent === dir) break;
922
+ dir = parent;
923
+ }
924
+ }
925
+
926
+ // Fallback to external module cache
927
+ if (!marketplacePath) {
928
+ const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
929
+ marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
930
+ }
931
+
932
+ try {
933
+ if (await fs.pathExists(marketplacePath)) {
934
+ const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
935
+ const plugins = data?.plugins;
936
+ if (!Array.isArray(plugins) || plugins.length === 0) return null;
937
+ let best = null;
938
+ for (const p of plugins) {
939
+ if (p.version && (!best || p.version > best)) best = p.version;
940
+ }
941
+ return best;
942
+ }
943
+ } catch {
944
+ // ignore
945
+ }
946
+ return null;
947
+ }
948
+
923
949
  /**
924
950
  * Fetch latest version from npm for a package
925
951
  * @param {string} packageName - npm package name
@@ -86,7 +86,7 @@ class ConfigDrivenIdeSetup {
86
86
  if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
87
87
 
88
88
  // Clean up any old BMAD installation first
89
- await this.cleanup(projectDir, options);
89
+ await this.cleanup(projectDir, options, bmadDir);
90
90
 
91
91
  if (!this.installerConfig) {
92
92
  return { success: false, reason: 'no-config' };
@@ -215,15 +215,34 @@ class ConfigDrivenIdeSetup {
215
215
  * Cleanup IDE configuration
216
216
  * @param {string} projectDir - Project directory
217
217
  */
218
- async cleanup(projectDir, options = {}) {
218
+ async cleanup(projectDir, options = {}, bmadDir = null) {
219
+ const resolvedBmadDir = bmadDir || (await this._findBmadDir(projectDir));
220
+
221
+ // Build removal set: previously installed skills + removals.txt entries
222
+ let removalSet;
223
+ if (options.previousSkillIds && options.previousSkillIds.size > 0) {
224
+ // Install/update flow: use pre-captured skill IDs (before manifest was overwritten)
225
+ removalSet = new Set(options.previousSkillIds);
226
+ if (resolvedBmadDir) {
227
+ const removals = await this.loadRemovalLists(resolvedBmadDir);
228
+ for (const entry of removals) removalSet.add(entry);
229
+ }
230
+ } else if (resolvedBmadDir) {
231
+ // Uninstall flow: read from current skill-manifest.csv + removals.txt
232
+ removalSet = await this._buildUninstallSet(resolvedBmadDir);
233
+ } else {
234
+ removalSet = new Set();
235
+ }
236
+
219
237
  // Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
238
+ // Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
220
239
  if (this.installerConfig?.legacy_targets) {
221
240
  if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
222
241
  for (const legacyDir of this.installerConfig.legacy_targets) {
223
242
  if (this.isGlobalPath(legacyDir)) {
224
243
  await this.warnGlobalLegacy(legacyDir, options);
225
244
  } else {
226
- await this.cleanupTarget(projectDir, legacyDir, options);
245
+ await this.cleanupTarget(projectDir, legacyDir, options, null);
227
246
  await this.removeEmptyParents(projectDir, legacyDir);
228
247
  }
229
248
  }
@@ -244,9 +263,9 @@ class ConfigDrivenIdeSetup {
244
263
  await this.cleanupRovoDevPrompts(projectDir, options);
245
264
  }
246
265
 
247
- // Clean target directory
266
+ // Clean current target directory
248
267
  if (this.installerConfig?.target_dir) {
249
- await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options);
268
+ await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet);
250
269
  }
251
270
  }
252
271
 
@@ -286,23 +305,117 @@ class ConfigDrivenIdeSetup {
286
305
  }
287
306
 
288
307
  /**
289
- * Cleanup a specific target directory
308
+ * Find the _bmad directory in a project
309
+ * @param {string} projectDir - Project directory
310
+ * @returns {string|null} Path to bmad dir or null
311
+ */
312
+ async _findBmadDir(projectDir) {
313
+ const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
314
+ return (await fs.pathExists(bmadDir)) ? bmadDir : null;
315
+ }
316
+
317
+ /**
318
+ * Build the full set of entries to remove for uninstall.
319
+ * Reads skill-manifest.csv to know exactly what was installed, plus removal lists.
320
+ * @param {string} bmadDir - BMAD installation directory
321
+ * @returns {Set<string>} Set of entries to remove
322
+ */
323
+ async _buildUninstallSet(bmadDir) {
324
+ const removals = await this.loadRemovalLists(bmadDir);
325
+
326
+ // Also add all currently installed skills from skill-manifest.csv
327
+ const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
328
+ try {
329
+ if (await fs.pathExists(csvPath)) {
330
+ const content = await fs.readFile(csvPath, 'utf8');
331
+ const records = csv.parse(content, { columns: true, skip_empty_lines: true });
332
+ for (const record of records) {
333
+ if (record.canonicalId) {
334
+ removals.add(record.canonicalId);
335
+ }
336
+ }
337
+ }
338
+ } catch {
339
+ // If we can't read the manifest, we still have the removal lists
340
+ }
341
+
342
+ return removals;
343
+ }
344
+
345
+ /**
346
+ * Load removal lists from all module sources in the bmad directory.
347
+ * Each module can have an optional removals.txt listing entries to remove.
348
+ * @param {string} bmadDir - BMAD installation directory
349
+ * @returns {Set<string>} Set of entries to remove
350
+ */
351
+ async loadRemovalLists(bmadDir) {
352
+ const removals = new Set();
353
+ const { getProjectRoot } = require('../project-root');
354
+
355
+ // Read project-level removals.txt (covers core and bmm)
356
+ const projectRemovalsPath = path.join(getProjectRoot(), 'removals.txt');
357
+ await this._readRemovalFile(projectRemovalsPath, removals);
358
+
359
+ // Read per-module removals.txt from installed module directories
360
+ try {
361
+ const entries = await fs.readdir(bmadDir);
362
+ for (const entry of entries) {
363
+ if (entry.startsWith('_')) continue;
364
+ const removalPath = path.join(bmadDir, entry, 'removals.txt');
365
+ await this._readRemovalFile(removalPath, removals);
366
+ }
367
+ } catch {
368
+ // bmadDir may not exist yet on fresh install
369
+ }
370
+
371
+ return removals;
372
+ }
373
+
374
+ /**
375
+ * Read a removals.txt file and add entries to the set
376
+ * @param {string} filePath - Path to removals.txt
377
+ * @param {Set<string>} removals - Set to add entries to
378
+ */
379
+ async _readRemovalFile(filePath, removals) {
380
+ try {
381
+ if (await fs.pathExists(filePath)) {
382
+ const content = await fs.readFile(filePath, 'utf8');
383
+ for (const line of content.split('\n')) {
384
+ const trimmed = line.trim();
385
+ if (trimmed && !trimmed.startsWith('#')) {
386
+ removals.add(trimmed);
387
+ }
388
+ }
389
+ }
390
+ } catch {
391
+ // Optional file — ignore errors
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Cleanup a specific target directory.
397
+ * When removalSet is provided, only removes entries in that set.
398
+ * When removalSet is null (legacy dirs), removes all bmad-prefixed entries.
290
399
  * @param {string} projectDir - Project directory
291
400
  * @param {string} targetDir - Target directory to clean
401
+ * @param {Object} options - Cleanup options
402
+ * @param {Set<string>|null} removalSet - Entries to remove, or null for legacy prefix matching
292
403
  */
293
- async cleanupTarget(projectDir, targetDir, options = {}) {
404
+ async cleanupTarget(projectDir, targetDir, options = {}, removalSet = new Set()) {
294
405
  const targetPath = path.join(projectDir, targetDir);
295
406
 
296
407
  if (!(await fs.pathExists(targetPath))) {
297
408
  return;
298
409
  }
299
410
 
300
- // Remove all bmad* files
411
+ if (removalSet && removalSet.size === 0) {
412
+ return;
413
+ }
414
+
301
415
  let entries;
302
416
  try {
303
417
  entries = await fs.readdir(targetPath);
304
418
  } catch {
305
- // Directory exists but can't be read - skip cleanup
306
419
  return;
307
420
  }
308
421
 
@@ -313,23 +426,26 @@ class ConfigDrivenIdeSetup {
313
426
  let removedCount = 0;
314
427
 
315
428
  for (const entry of entries) {
316
- if (!entry || typeof entry !== 'string') {
317
- continue;
318
- }
319
- if (entry.startsWith('bmad') && !entry.startsWith('bmad-os-')) {
320
- const entryPath = path.join(targetPath, entry);
429
+ if (!entry || typeof entry !== 'string') continue;
430
+
431
+ // Always preserve bmad-os-* utility skills regardless of cleanup mode
432
+ if (entry.startsWith('bmad-os-')) continue;
433
+
434
+ // Surgical removal from set, or legacy prefix matching when set is null
435
+ const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
436
+
437
+ if (shouldRemove) {
321
438
  try {
322
- await fs.remove(entryPath);
439
+ await fs.remove(path.join(targetPath, entry));
323
440
  removedCount++;
324
441
  } catch {
325
- // Skip entries that can't be removed (broken symlinks, permission errors)
442
+ // Skip entries that can't be removed
326
443
  }
327
444
  }
328
445
  }
329
446
 
330
- if (removedCount > 0 && !options.silent) {
331
- await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`);
332
- }
447
+ // Only log cleanup when it's not a routine reinstall (legacy dir cleanup or actual removals)
448
+ // Suppress for current target_dir since it's always cleaned before a fresh write
333
449
 
334
450
  // Remove empty directory after cleanup
335
451
  if (removedCount > 0) {
@@ -339,7 +455,7 @@ class ConfigDrivenIdeSetup {
339
455
  await fs.remove(targetPath);
340
456
  }
341
457
  } catch {
342
- // Directory may already be gone or in use — skip
458
+ // Directory may already be gone or in use
343
459
  }
344
460
  }
345
461
  }
@@ -6,32 +6,25 @@
6
6
  startMessage: |
7
7
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8
8
 
9
- 🎉 V6 IS HERE! Welcome to BMad Method V6 - Official Stable Release!
10
-
11
- The BMad Method is now a Platform powered by the BMad Method Core and Module Ecosystem!
12
- - Select and install modules during setup - customize your experience
13
- - New BMad Method for Agile AI-Driven Development (the evolution of V4)
14
- - Exciting new modules available during installation, with community modules coming soon
15
- - Documentation: https://docs.bmad-method.org
16
-
17
- 🌟 BMad is 100% free and open source.
18
- - No gated Discord. No paywalls. No gated content.
19
- - We believe in empowering everyone, not just those who can pay.
20
- - Knowledge should be shared, not sold.
21
-
22
- 🎤 SPEAKING & MEDIA:
23
- - Available for conferences, podcasts, and media appearances
24
- - Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
25
- - For speaking inquiries or interviews, reach out to BMad on Discord!
26
-
27
- HELP US GROW:
28
- - Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
29
- - Subscribe on YouTube: https://www.youtube.com/@BMadCode
30
- - Free Community and Support: https://discord.gg/gk8jAdXWmj
31
- - Donate: https://buymeacoffee.com/bmad
32
- - Corporate Sponsorship available
33
-
34
- Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/blob/main/CHANGELOG.md
9
+ Agile AI-Driven Development. Powered by BMad Core and a growing module ecosystem.
10
+ Install official and community modules during setup to customize your experience.
11
+
12
+ 🌟 100% free. 100% open source. Always.
13
+ No paywalls. No gated content. Knowledge shared, not sold.
14
+
15
+ 🌐 CONNECT:
16
+ Website: https://bmadcode.com/
17
+ Discord: https://discord.gg/gk8jAdXWmj
18
+ YouTube: https://www.youtube.com/@BMadCode
19
+ X: https://x.com/BMadCode
20
+ Facebook: https://facebook.com/@BMadCode
21
+
22
+ SUPPORT THE PROJECT:
23
+ Star us: https://github.com/bmad-code-org/BMAD-METHOD/
24
+ Donate: https://buymeacoffee.com/bmad
25
+ Corporate sponsorship and speaking inquiries: contact@bmadcode.com
26
+
27
+ Docs, blog, and latest updates: https://bmadcode.com/
35
28
 
36
29
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
37
30
 
@@ -4,8 +4,50 @@ const fs = require('fs-extra');
4
4
  const { CLIUtils } = require('./cli-utils');
5
5
  const { CustomHandler } = require('./custom-handler');
6
6
  const { ExternalModuleManager } = require('./modules/external-manager');
7
+ const { getProjectRoot } = require('./project-root');
7
8
  const prompts = require('./prompts');
8
9
 
10
+ /**
11
+ * Read module version from .claude-plugin/marketplace.json
12
+ * @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis')
13
+ * @returns {string} Version string or empty string
14
+ */
15
+ async function getMarketplaceVersion(moduleCode) {
16
+ let marketplacePath;
17
+ if (moduleCode === 'core' || moduleCode === 'bmm') {
18
+ marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
19
+ } else {
20
+ const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleCode);
21
+ marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
22
+ }
23
+ try {
24
+ if (await fs.pathExists(marketplacePath)) {
25
+ const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
26
+ return _extractMarketplaceVersion(data);
27
+ }
28
+ } catch {
29
+ // ignore
30
+ }
31
+ return '';
32
+ }
33
+
34
+ /**
35
+ * Extract the highest version from marketplace.json plugins array.
36
+ * Handles multiple plugins per file safely.
37
+ * @param {Object} data - Parsed marketplace.json
38
+ * @returns {string} Version string or empty string
39
+ */
40
+ function _extractMarketplaceVersion(data) {
41
+ const plugins = data?.plugins;
42
+ if (!Array.isArray(plugins) || plugins.length === 0) return '';
43
+ // Use the highest version across all plugins in the file
44
+ let best = '';
45
+ for (const p of plugins) {
46
+ if (p.version && (!best || p.version > best)) best = p.version;
47
+ }
48
+ return best;
49
+ }
50
+
9
51
  // Separator class for visual grouping in select/multiselect prompts
10
52
  // Note: @clack/prompts doesn't support separators natively, they are filtered out
11
53
  class Separator {
@@ -70,17 +112,14 @@ class UI {
70
112
  if (hasExistingInstall) {
71
113
  // Get version information
72
114
  const { existingInstall, bmadDir } = await this.getExistingInstallation(confirmedDirectory);
73
- const packageJsonPath = path.join(__dirname, '../../package.json');
74
- const currentVersion = require(packageJsonPath).version;
75
- const installedVersion = existingInstall.installed ? existingInstall.version || 'unknown' : 'unknown';
76
115
 
77
116
  // Build menu choices dynamically
78
117
  const choices = [];
79
118
 
80
119
  // Always show Quick Update first (allows refreshing installation even on same version)
81
- if (installedVersion !== 'unknown') {
120
+ if (existingInstall.installed) {
82
121
  choices.push({
83
- name: `Quick Update (v${installedVersion} → v${currentVersion})`,
122
+ name: 'Quick Update',
84
123
  value: 'quick-update',
85
124
  });
86
125
  }
@@ -880,14 +919,18 @@ class UI {
880
919
  const lockedValues = ['core'];
881
920
 
882
921
  // Core module is always installed — show it locked at the top
883
- allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' });
922
+ const coreVersion = await getMarketplaceVersion('core');
923
+ const coreLabel = coreVersion ? `BMad Core Module (v${coreVersion})` : 'BMad Core Module';
924
+ allOptions.push({ label: coreLabel, value: 'core', hint: 'Core configuration and shared resources' });
884
925
  initialValues.push('core');
885
926
 
886
927
  // Helper to build module entry with proper sorting and selection
887
- const buildModuleEntry = (mod, value, group) => {
928
+ const buildModuleEntry = async (mod, value, group) => {
888
929
  const isInstalled = installedModuleIds.has(value);
930
+ const version = await getMarketplaceVersion(value);
931
+ const label = version ? `${mod.name} (v${version})` : mod.name;
889
932
  return {
890
- label: mod.name,
933
+ label,
891
934
  value,
892
935
  hint: mod.description || group,
893
936
  // Pre-select only if already installed (not on fresh install)
@@ -899,7 +942,7 @@ class UI {
899
942
  const localEntries = [];
900
943
  for (const mod of localModules) {
901
944
  if (!mod.isCustom && mod.id !== 'core') {
902
- const entry = buildModuleEntry(mod, mod.id, 'Local');
945
+ const entry = await buildModuleEntry(mod, mod.id, 'Local');
903
946
  localEntries.push(entry);
904
947
  if (entry.selected) {
905
948
  initialValues.push(mod.id);
@@ -912,7 +955,7 @@ class UI {
912
955
  const officialModules = [];
913
956
  for (const mod of externalModules) {
914
957
  if (mod.type === 'bmad-org') {
915
- const entry = buildModuleEntry(mod, mod.code, 'Official');
958
+ const entry = await buildModuleEntry(mod, mod.code, 'Official');
916
959
  officialModules.push(entry);
917
960
  if (entry.selected) {
918
961
  initialValues.push(mod.code);
@@ -925,7 +968,7 @@ class UI {
925
968
  const communityModules = [];
926
969
  for (const mod of externalModules) {
927
970
  if (mod.type === 'community') {
928
- const entry = buildModuleEntry(mod, mod.code, 'Community');
971
+ const entry = await buildModuleEntry(mod, mod.code, 'Community');
929
972
  communityModules.push(entry);
930
973
  if (entry.selected) {
931
974
  initialValues.push(mod.code);