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 +7 -8
- package/package.json +1 -1
- package/removals.txt +17 -0
- package/tools/installer/cli-utils.js +18 -9
- package/tools/installer/core/installer.js +112 -33
- package/tools/installer/core/manifest.js +61 -35
- package/tools/installer/ide/_config-driven.js +136 -20
- package/tools/installer/install-messages.yaml +19 -26
- package/tools/installer/ui.js +54 -11
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/bmad-method)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
|
+
[](https://www.python.org)
|
|
7
|
+
[](https://docs.astral.sh/uv/)
|
|
6
8
|
[](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
|
-
- [
|
|
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
|
|
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
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
|
-
|
|
26
|
-
|
|
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
|
|
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}`,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: ${
|
|
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
|
-
'
|
|
1115
|
-
`
|
|
1116
|
-
`
|
|
1117
|
-
|
|
1118
|
-
`
|
|
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.
|
|
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
|
-
//
|
|
842
|
+
// Resolve source type first, then read version with the correct path context
|
|
844
843
|
if (['core', 'bmm'].includes(moduleName)) {
|
|
845
|
-
const
|
|
844
|
+
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
|
846
845
|
return {
|
|
847
|
-
version
|
|
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
|
|
861
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
if (entry.startsWith('bmad
|
|
320
|
-
|
|
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(
|
|
439
|
+
await fs.remove(path.join(targetPath, entry));
|
|
323
440
|
removedCount++;
|
|
324
441
|
} catch {
|
|
325
|
-
// Skip entries that can't be removed
|
|
442
|
+
// Skip entries that can't be removed
|
|
326
443
|
}
|
|
327
444
|
}
|
|
328
445
|
}
|
|
329
446
|
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
package/tools/installer/ui.js
CHANGED
|
@@ -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 (
|
|
120
|
+
if (existingInstall.installed) {
|
|
82
121
|
choices.push({
|
|
83
|
-
name:
|
|
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
|
-
|
|
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
|
|
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);
|