bmad-method 6.2.3-next.9 → 6.3.0
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/.claude-plugin/marketplace.json +0 -3
- package/README.md +8 -9
- package/README_CN.md +1 -1
- package/README_VN.md +110 -0
- package/package.json +1 -1
- package/removals.txt +17 -0
- package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-13-responsive-accessibility.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/data/prd-purpose.md +197 -0
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01-discovery.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01b-legacy-conversion.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-02-review.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-03-edit.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +1 -3
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-01-document-discovery.md +1 -1
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-02-prd-analysis.md +1 -1
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-03-epic-coverage-validation.md +1 -1
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +1 -1
- package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +5 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/SKILL.md +29 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/generate-trail.md +38 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-01-orientation.md +105 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-02-walkthrough.md +89 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-03-detail-pass.md +106 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-04-testing.md +74 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-05-wrapup.md +24 -0
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-01-gather-context.md +38 -15
- package/src/bmm-skills/4-implementation/bmad-correct-course/checklist.md +2 -2
- package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +8 -8
- package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/checklist.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-quick-dev/compile-epic-context.md +62 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/spec-template.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md +33 -6
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-02-plan.md +20 -8
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-03-implement.md +2 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +16 -4
- package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +1 -5
- package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +134 -134
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/sprint-status-template.yaml +1 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/workflow.md +3 -3
- package/src/bmm-skills/4-implementation/bmad-sprint-status/workflow.md +2 -2
- package/src/bmm-skills/module-help.csv +2 -0
- package/src/core-skills/bmad-help/SKILL.md +4 -2
- package/src/core-skills/bmad-party-mode/SKILL.md +8 -6
- package/src/core-skills/module-help.csv +1 -0
- package/tools/installer/cli-utils.js +18 -9
- package/tools/installer/commands/install.js +1 -1
- package/tools/installer/core/existing-install.js +2 -8
- package/tools/installer/core/install-paths.js +0 -3
- package/tools/installer/core/installer.js +180 -463
- package/tools/installer/core/manifest-generator.js +8 -14
- package/tools/installer/core/manifest.js +94 -102
- package/tools/installer/ide/_config-driven.js +149 -38
- package/tools/installer/ide/shared/skill-manifest.js +1 -16
- package/tools/installer/install-messages.yaml +19 -26
- package/tools/installer/modules/community-manager.js +377 -0
- package/tools/installer/modules/custom-module-manager.js +644 -0
- package/tools/installer/modules/external-manager.js +65 -49
- package/tools/installer/modules/official-modules.js +117 -65
- package/tools/installer/modules/plugin-resolver.js +398 -0
- package/tools/installer/modules/registry-client.js +66 -0
- package/tools/installer/{external-official-modules.yaml → modules/registry-fallback.yaml} +3 -12
- package/tools/installer/ui.js +549 -666
- package/src/bmm-skills/4-implementation/bmad-agent-qa/SKILL.md +0 -61
- package/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/SKILL.md +0 -53
- package/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/4-implementation/bmad-agent-sm/SKILL.md +0 -55
- package/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml +0 -11
- package/tools/installer/core/custom-module-cache.js +0 -260
- package/tools/installer/custom-handler.js +0 -112
- package/tools/installer/modules/custom-modules.js +0 -197
package/tools/installer/ui.js
CHANGED
|
@@ -2,22 +2,50 @@ const path = require('node:path');
|
|
|
2
2
|
const os = require('node:os');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const { CLIUtils } = require('./cli-utils');
|
|
5
|
-
const { CustomHandler } = require('./custom-handler');
|
|
6
5
|
const { ExternalModuleManager } = require('./modules/external-manager');
|
|
6
|
+
const { getProjectRoot } = require('./project-root');
|
|
7
7
|
const prompts = require('./prompts');
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Read module version from .claude-plugin/marketplace.json
|
|
11
|
+
* @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis')
|
|
12
|
+
* @returns {string} Version string or empty string
|
|
13
|
+
*/
|
|
14
|
+
async function getMarketplaceVersion(moduleCode) {
|
|
15
|
+
let marketplacePath;
|
|
16
|
+
if (moduleCode === 'core' || moduleCode === 'bmm') {
|
|
17
|
+
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
|
18
|
+
} else {
|
|
19
|
+
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleCode);
|
|
20
|
+
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
if (await fs.pathExists(marketplacePath)) {
|
|
24
|
+
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
|
25
|
+
return _extractMarketplaceVersion(data);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// ignore
|
|
15
29
|
}
|
|
16
|
-
|
|
30
|
+
return '';
|
|
17
31
|
}
|
|
18
32
|
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Extract the highest version from marketplace.json plugins array.
|
|
35
|
+
* Handles multiple plugins per file safely.
|
|
36
|
+
* @param {Object} data - Parsed marketplace.json
|
|
37
|
+
* @returns {string} Version string or empty string
|
|
38
|
+
*/
|
|
39
|
+
function _extractMarketplaceVersion(data) {
|
|
40
|
+
const plugins = data?.plugins;
|
|
41
|
+
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
|
42
|
+
// Use the highest version across all plugins in the file
|
|
43
|
+
let best = '';
|
|
44
|
+
for (const p of plugins) {
|
|
45
|
+
if (p.version && (!best || p.version > best)) best = p.version;
|
|
46
|
+
}
|
|
47
|
+
return best;
|
|
48
|
+
}
|
|
21
49
|
|
|
22
50
|
/**
|
|
23
51
|
* UI utilities for the installer
|
|
@@ -58,11 +86,6 @@ class UI {
|
|
|
58
86
|
// Check if there's an existing BMAD installation
|
|
59
87
|
const hasExistingInstall = await fs.pathExists(bmadDir);
|
|
60
88
|
|
|
61
|
-
let customContentConfig = { hasCustomContent: false };
|
|
62
|
-
if (!hasExistingInstall) {
|
|
63
|
-
customContentConfig._shouldAsk = true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
89
|
// Track action type (only set if there's an existing installation)
|
|
67
90
|
let actionType;
|
|
68
91
|
|
|
@@ -70,17 +93,14 @@ class UI {
|
|
|
70
93
|
if (hasExistingInstall) {
|
|
71
94
|
// Get version information
|
|
72
95
|
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
96
|
|
|
77
97
|
// Build menu choices dynamically
|
|
78
98
|
const choices = [];
|
|
79
99
|
|
|
80
100
|
// Always show Quick Update first (allows refreshing installation even on same version)
|
|
81
|
-
if (
|
|
101
|
+
if (existingInstall.installed) {
|
|
82
102
|
choices.push({
|
|
83
|
-
name:
|
|
103
|
+
name: 'Quick Update',
|
|
84
104
|
value: 'quick-update',
|
|
85
105
|
});
|
|
86
106
|
}
|
|
@@ -114,48 +134,9 @@ class UI {
|
|
|
114
134
|
|
|
115
135
|
// Handle quick update separately
|
|
116
136
|
if (actionType === 'quick-update') {
|
|
117
|
-
// Pass --custom-content through so installer can re-cache if cache is missing
|
|
118
|
-
let customContentForQuickUpdate = { hasCustomContent: false };
|
|
119
|
-
if (options.customContent) {
|
|
120
|
-
const paths = options.customContent
|
|
121
|
-
.split(',')
|
|
122
|
-
.map((p) => p.trim())
|
|
123
|
-
.filter(Boolean);
|
|
124
|
-
if (paths.length > 0) {
|
|
125
|
-
const customPaths = [];
|
|
126
|
-
const selectedModuleIds = [];
|
|
127
|
-
const sources = [];
|
|
128
|
-
for (const customPath of paths) {
|
|
129
|
-
const expandedPath = this.expandUserPath(customPath);
|
|
130
|
-
const validation = this.validateCustomContentPathSync(expandedPath);
|
|
131
|
-
if (validation) continue;
|
|
132
|
-
let moduleMeta;
|
|
133
|
-
try {
|
|
134
|
-
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
135
|
-
moduleMeta = require('yaml').parse(await fs.readFile(moduleYamlPath, 'utf-8'));
|
|
136
|
-
} catch {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (!moduleMeta?.code) continue;
|
|
140
|
-
customPaths.push(expandedPath);
|
|
141
|
-
selectedModuleIds.push(moduleMeta.code);
|
|
142
|
-
sources.push({ path: expandedPath, id: moduleMeta.code, name: moduleMeta.name || moduleMeta.code });
|
|
143
|
-
}
|
|
144
|
-
if (customPaths.length > 0) {
|
|
145
|
-
customContentForQuickUpdate = {
|
|
146
|
-
hasCustomContent: true,
|
|
147
|
-
selected: true,
|
|
148
|
-
sources,
|
|
149
|
-
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
|
|
150
|
-
selectedModuleIds,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
137
|
return {
|
|
156
138
|
actionType: 'quick-update',
|
|
157
139
|
directory: confirmedDirectory,
|
|
158
|
-
customContent: customContentForQuickUpdate,
|
|
159
140
|
skipPrompts: options.yes || false,
|
|
160
141
|
};
|
|
161
142
|
}
|
|
@@ -177,6 +158,9 @@ class UI {
|
|
|
177
158
|
.map((m) => m.trim())
|
|
178
159
|
.filter(Boolean);
|
|
179
160
|
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
|
161
|
+
} else if (options.customSource) {
|
|
162
|
+
// Custom source without --modules: start with empty list (core added below)
|
|
163
|
+
selectedModules = [];
|
|
180
164
|
} else if (options.yes) {
|
|
181
165
|
selectedModules = await this.getDefaultModules(installedModuleIds);
|
|
182
166
|
await prompts.log.info(
|
|
@@ -186,120 +170,14 @@ class UI {
|
|
|
186
170
|
selectedModules = await this.selectAllModules(installedModuleIds);
|
|
187
171
|
}
|
|
188
172
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const paths = options.customContent
|
|
195
|
-
.split(',')
|
|
196
|
-
.map((p) => p.trim())
|
|
197
|
-
.filter(Boolean);
|
|
198
|
-
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
|
|
199
|
-
|
|
200
|
-
// Build custom content config similar to promptCustomContentSource
|
|
201
|
-
const customPaths = [];
|
|
202
|
-
const selectedModuleIds = [];
|
|
203
|
-
const sources = [];
|
|
204
|
-
|
|
205
|
-
for (const customPath of paths) {
|
|
206
|
-
const expandedPath = this.expandUserPath(customPath);
|
|
207
|
-
const validation = this.validateCustomContentPathSync(expandedPath);
|
|
208
|
-
if (validation) {
|
|
209
|
-
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Read module metadata
|
|
214
|
-
let moduleMeta;
|
|
215
|
-
try {
|
|
216
|
-
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
217
|
-
const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
|
|
218
|
-
const yaml = require('yaml');
|
|
219
|
-
moduleMeta = yaml.parse(moduleYaml);
|
|
220
|
-
} catch (error) {
|
|
221
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (!moduleMeta) {
|
|
226
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!moduleMeta.code) {
|
|
231
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
customPaths.push(expandedPath);
|
|
236
|
-
selectedModuleIds.push(moduleMeta.code);
|
|
237
|
-
sources.push({
|
|
238
|
-
path: expandedPath,
|
|
239
|
-
id: moduleMeta.code,
|
|
240
|
-
name: moduleMeta.name || moduleMeta.code,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (customPaths.length > 0) {
|
|
245
|
-
customModuleResult = {
|
|
246
|
-
selectedCustomModules: selectedModuleIds,
|
|
247
|
-
customContentConfig: {
|
|
248
|
-
hasCustomContent: true,
|
|
249
|
-
selected: true,
|
|
250
|
-
sources,
|
|
251
|
-
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
|
|
252
|
-
selectedModuleIds: selectedModuleIds,
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
} else if (options.yes) {
|
|
257
|
-
// Non-interactive mode: preserve existing custom modules (matches default: false)
|
|
258
|
-
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
259
|
-
if (await fs.pathExists(cacheDir)) {
|
|
260
|
-
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
261
|
-
for (const entry of entries) {
|
|
262
|
-
if (entry.isDirectory()) {
|
|
263
|
-
customModuleResult.selectedCustomModules.push(entry.name);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
await prompts.log.info(
|
|
267
|
-
`Non-interactive mode (--yes): preserving ${customModuleResult.selectedCustomModules.length} existing custom module(s)`,
|
|
268
|
-
);
|
|
269
|
-
} else {
|
|
270
|
-
await prompts.log.info('Non-interactive mode (--yes): no existing custom modules found');
|
|
271
|
-
}
|
|
272
|
-
} else {
|
|
273
|
-
const changeCustomModules = await prompts.confirm({
|
|
274
|
-
message: 'Modify custom modules, agents, or workflows?',
|
|
275
|
-
default: false,
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
if (changeCustomModules) {
|
|
279
|
-
customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
|
|
280
|
-
} else {
|
|
281
|
-
// Preserve existing custom modules if user doesn't want to modify them
|
|
282
|
-
const { Installer } = require('./core/installer');
|
|
283
|
-
const installer = new Installer();
|
|
284
|
-
const { bmadDir } = await installer.findBmadDir(confirmedDirectory);
|
|
285
|
-
|
|
286
|
-
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
287
|
-
if (await fs.pathExists(cacheDir)) {
|
|
288
|
-
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
289
|
-
for (const entry of entries) {
|
|
290
|
-
if (entry.isDirectory()) {
|
|
291
|
-
customModuleResult.selectedCustomModules.push(entry.name);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
173
|
+
// Resolve custom sources from --custom-source flag
|
|
174
|
+
if (options.customSource) {
|
|
175
|
+
const customCodes = await this._resolveCustomSourcesCli(options.customSource);
|
|
176
|
+
for (const code of customCodes) {
|
|
177
|
+
if (!selectedModules.includes(code)) selectedModules.push(code);
|
|
295
178
|
}
|
|
296
179
|
}
|
|
297
180
|
|
|
298
|
-
// Merge any selected custom modules
|
|
299
|
-
if (customModuleResult.selectedCustomModules.length > 0) {
|
|
300
|
-
selectedModules.push(...customModuleResult.selectedCustomModules);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
181
|
// Ensure core is in the modules list
|
|
304
182
|
if (!selectedModules.includes('core')) {
|
|
305
183
|
selectedModules.unshift('core');
|
|
@@ -318,7 +196,6 @@ class UI {
|
|
|
318
196
|
skipIde: toolSelection.skipIde,
|
|
319
197
|
coreConfig: moduleConfigs.core || {},
|
|
320
198
|
moduleConfigs: moduleConfigs,
|
|
321
|
-
customContent: customModuleResult.customContentConfig,
|
|
322
199
|
skipPrompts: options.yes || false,
|
|
323
200
|
};
|
|
324
201
|
}
|
|
@@ -336,6 +213,9 @@ class UI {
|
|
|
336
213
|
.map((m) => m.trim())
|
|
337
214
|
.filter(Boolean);
|
|
338
215
|
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
|
216
|
+
} else if (options.customSource) {
|
|
217
|
+
// Custom source without --modules: start with empty list (core added below)
|
|
218
|
+
selectedModules = [];
|
|
339
219
|
} else if (options.yes) {
|
|
340
220
|
// Use default modules when --yes flag is set
|
|
341
221
|
selectedModules = await this.getDefaultModules(installedModuleIds);
|
|
@@ -344,84 +224,14 @@ class UI {
|
|
|
344
224
|
selectedModules = await this.selectAllModules(installedModuleIds);
|
|
345
225
|
}
|
|
346
226
|
|
|
347
|
-
//
|
|
348
|
-
if (options.
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
.
|
|
352
|
-
.map((p) => p.trim())
|
|
353
|
-
.filter(Boolean);
|
|
354
|
-
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
|
|
355
|
-
|
|
356
|
-
// Build custom content config similar to promptCustomContentSource
|
|
357
|
-
const customPaths = [];
|
|
358
|
-
const selectedModuleIds = [];
|
|
359
|
-
const sources = [];
|
|
360
|
-
|
|
361
|
-
for (const customPath of paths) {
|
|
362
|
-
const expandedPath = this.expandUserPath(customPath);
|
|
363
|
-
const validation = this.validateCustomContentPathSync(expandedPath);
|
|
364
|
-
if (validation) {
|
|
365
|
-
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Read module metadata
|
|
370
|
-
let moduleMeta;
|
|
371
|
-
try {
|
|
372
|
-
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
373
|
-
const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
|
|
374
|
-
const yaml = require('yaml');
|
|
375
|
-
moduleMeta = yaml.parse(moduleYaml);
|
|
376
|
-
} catch (error) {
|
|
377
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (!moduleMeta) {
|
|
382
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (!moduleMeta.code) {
|
|
387
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
customPaths.push(expandedPath);
|
|
392
|
-
selectedModuleIds.push(moduleMeta.code);
|
|
393
|
-
sources.push({
|
|
394
|
-
path: expandedPath,
|
|
395
|
-
id: moduleMeta.code,
|
|
396
|
-
name: moduleMeta.name || moduleMeta.code,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (customPaths.length > 0) {
|
|
401
|
-
customContentConfig = {
|
|
402
|
-
hasCustomContent: true,
|
|
403
|
-
selected: true,
|
|
404
|
-
sources,
|
|
405
|
-
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
|
|
406
|
-
selectedModuleIds: selectedModuleIds,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
} else if (!options.yes) {
|
|
410
|
-
const wantsCustomContent = await prompts.confirm({
|
|
411
|
-
message: 'Add custom modules, agents, or workflows from your computer?',
|
|
412
|
-
default: false,
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
if (wantsCustomContent) {
|
|
416
|
-
customContentConfig = await this.promptCustomContentSource();
|
|
227
|
+
// Resolve custom sources from --custom-source flag
|
|
228
|
+
if (options.customSource) {
|
|
229
|
+
const customCodes = await this._resolveCustomSourcesCli(options.customSource);
|
|
230
|
+
for (const code of customCodes) {
|
|
231
|
+
if (!selectedModules.includes(code)) selectedModules.push(code);
|
|
417
232
|
}
|
|
418
233
|
}
|
|
419
234
|
|
|
420
|
-
// Add custom content modules if any were selected
|
|
421
|
-
if (customContentConfig && customContentConfig.selectedModuleIds) {
|
|
422
|
-
selectedModules.push(...customContentConfig.selectedModuleIds);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
235
|
// Ensure core is in the modules list
|
|
426
236
|
if (!selectedModules.includes('core')) {
|
|
427
237
|
selectedModules.unshift('core');
|
|
@@ -437,7 +247,6 @@ class UI {
|
|
|
437
247
|
skipIde: toolSelection.skipIde,
|
|
438
248
|
coreConfig: moduleConfigs.core || {},
|
|
439
249
|
moduleConfigs: moduleConfigs,
|
|
440
|
-
customContent: customContentConfig,
|
|
441
250
|
skipPrompts: options.yes || false,
|
|
442
251
|
};
|
|
443
252
|
}
|
|
@@ -776,166 +585,80 @@ class UI {
|
|
|
776
585
|
}
|
|
777
586
|
|
|
778
587
|
/**
|
|
779
|
-
*
|
|
588
|
+
* Select all modules across three tiers: official, community, and custom URL.
|
|
780
589
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
781
|
-
* @
|
|
782
|
-
* @returns {Array} Module choices for prompt
|
|
590
|
+
* @returns {Array} Selected module codes (excluding core)
|
|
783
591
|
*/
|
|
784
|
-
async
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
const isNewInstallation = installedModuleIds.size === 0;
|
|
788
|
-
|
|
789
|
-
const customContentItems = [];
|
|
790
|
-
|
|
791
|
-
// Add custom content items
|
|
792
|
-
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
|
|
793
|
-
// Existing installation - show from directory
|
|
794
|
-
const customHandler = new CustomHandler();
|
|
795
|
-
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
|
|
796
|
-
|
|
797
|
-
for (const customFile of customFiles) {
|
|
798
|
-
const customInfo = await customHandler.getCustomInfo(customFile);
|
|
799
|
-
if (customInfo) {
|
|
800
|
-
customContentItems.push({
|
|
801
|
-
name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`,
|
|
802
|
-
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
|
|
803
|
-
checked: true, // Default to selected since user chose to provide custom content
|
|
804
|
-
path: customInfo.path, // Track path to avoid duplicates
|
|
805
|
-
hint: customInfo.description || undefined,
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Add official modules
|
|
812
|
-
const { OfficialModules } = require('./modules/official-modules');
|
|
813
|
-
const officialModules = new OfficialModules();
|
|
814
|
-
const { modules: availableModules, customModules: customModulesFromCache } = await officialModules.listAvailable();
|
|
815
|
-
|
|
816
|
-
// First, add all items to appropriate sections
|
|
817
|
-
const allCustomModules = [];
|
|
818
|
-
|
|
819
|
-
// Add custom content items from directory
|
|
820
|
-
allCustomModules.push(...customContentItems);
|
|
821
|
-
|
|
822
|
-
// Add custom modules from cache
|
|
823
|
-
for (const mod of customModulesFromCache) {
|
|
824
|
-
// Skip if this module is already in customContentItems (by path)
|
|
825
|
-
const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path));
|
|
826
|
-
|
|
827
|
-
if (!isDuplicate) {
|
|
828
|
-
allCustomModules.push({
|
|
829
|
-
name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`,
|
|
830
|
-
value: mod.id,
|
|
831
|
-
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
|
832
|
-
hint: mod.description || undefined,
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Add separators and modules in correct order
|
|
838
|
-
if (allCustomModules.length > 0) {
|
|
839
|
-
// Add separator for custom content, all custom modules, and official content separator
|
|
840
|
-
moduleChoices.push(
|
|
841
|
-
new choiceUtils.Separator('── Custom Content ──'),
|
|
842
|
-
...allCustomModules,
|
|
843
|
-
new choiceUtils.Separator('── Official Content ──'),
|
|
844
|
-
);
|
|
845
|
-
}
|
|
592
|
+
async selectAllModules(installedModuleIds = new Set()) {
|
|
593
|
+
// Phase 1: Official modules
|
|
594
|
+
const officialSelected = await this._selectOfficialModules(installedModuleIds);
|
|
846
595
|
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
596
|
+
// Determine which installed modules are NOT official (community or custom).
|
|
597
|
+
// These must be preserved even if the user declines to browse community/custom.
|
|
598
|
+
const officialCodes = new Set(officialSelected);
|
|
599
|
+
const externalManager = new ExternalModuleManager();
|
|
600
|
+
const registryModules = await externalManager.listAvailable();
|
|
601
|
+
const officialRegistryCodes = new Set(registryModules.map((m) => m.code));
|
|
602
|
+
const installedNonOfficial = [...installedModuleIds].filter((id) => !officialRegistryCodes.has(id));
|
|
603
|
+
|
|
604
|
+
// Phase 2: Community modules (category drill-down)
|
|
605
|
+
// Returns { codes, didBrowse } so we know if the user entered the flow
|
|
606
|
+
const communityResult = await this._browseCommunityModules(installedModuleIds);
|
|
607
|
+
|
|
608
|
+
// Phase 3: Custom URL modules
|
|
609
|
+
const customSelected = await this._addCustomUrlModules(installedModuleIds);
|
|
610
|
+
|
|
611
|
+
// Merge all selections
|
|
612
|
+
const allSelected = new Set([...officialSelected, ...communityResult.codes, ...customSelected]);
|
|
613
|
+
|
|
614
|
+
// Auto-include installed non-official modules that the user didn't get
|
|
615
|
+
// a chance to manage (they declined to browse). If they did browse,
|
|
616
|
+
// trust their selections - they could have deselected intentionally.
|
|
617
|
+
if (!communityResult.didBrowse) {
|
|
618
|
+
for (const code of installedNonOfficial) {
|
|
619
|
+
allSelected.add(code);
|
|
856
620
|
}
|
|
857
621
|
}
|
|
858
622
|
|
|
859
|
-
return
|
|
623
|
+
return [...allSelected];
|
|
860
624
|
}
|
|
861
625
|
|
|
862
626
|
/**
|
|
863
|
-
* Select
|
|
864
|
-
*
|
|
627
|
+
* Select official modules using autocompleteMultiselect.
|
|
628
|
+
* Extracted from the original selectAllModules - unchanged behavior.
|
|
865
629
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
866
|
-
* @returns {Array} Selected module codes
|
|
630
|
+
* @returns {Array} Selected official module codes
|
|
867
631
|
*/
|
|
868
|
-
async
|
|
869
|
-
const { OfficialModules } = require('./modules/official-modules');
|
|
870
|
-
const officialModulesSource = new OfficialModules();
|
|
871
|
-
const { modules: localModules } = await officialModulesSource.listAvailable();
|
|
872
|
-
|
|
873
|
-
// Get external modules
|
|
632
|
+
async _selectOfficialModules(installedModuleIds = new Set()) {
|
|
874
633
|
const externalManager = new ExternalModuleManager();
|
|
875
|
-
const
|
|
634
|
+
const registryModules = await externalManager.listAvailable();
|
|
876
635
|
|
|
877
|
-
// Build flat options list with group hints for autocompleteMultiselect
|
|
878
636
|
const allOptions = [];
|
|
879
637
|
const initialValues = [];
|
|
880
638
|
const lockedValues = ['core'];
|
|
881
639
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
// Helper to build module entry with proper sorting and selection
|
|
887
|
-
const buildModuleEntry = (mod, value, group) => {
|
|
888
|
-
const isInstalled = installedModuleIds.has(value);
|
|
640
|
+
const buildModuleEntry = async (mod) => {
|
|
641
|
+
const isInstalled = installedModuleIds.has(mod.code);
|
|
642
|
+
const version = await getMarketplaceVersion(mod.code);
|
|
643
|
+
const label = version ? `${mod.name} (v${version})` : mod.name;
|
|
889
644
|
return {
|
|
890
|
-
label
|
|
891
|
-
value,
|
|
892
|
-
hint: mod.description
|
|
893
|
-
// Pre-select only if already installed (not on fresh install)
|
|
645
|
+
label,
|
|
646
|
+
value: mod.code,
|
|
647
|
+
hint: mod.description,
|
|
894
648
|
selected: isInstalled,
|
|
895
649
|
};
|
|
896
650
|
};
|
|
897
651
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
if (
|
|
902
|
-
|
|
903
|
-
localEntries.push(entry);
|
|
904
|
-
if (entry.selected) {
|
|
905
|
-
initialValues.push(mod.id);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
allOptions.push(...localEntries.map(({ label, value, hint }) => ({ label, value, hint })));
|
|
910
|
-
|
|
911
|
-
// Group 2: BMad Official Modules (type: bmad-org)
|
|
912
|
-
const officialModules = [];
|
|
913
|
-
for (const mod of externalModules) {
|
|
914
|
-
if (mod.type === 'bmad-org') {
|
|
915
|
-
const entry = buildModuleEntry(mod, mod.code, 'Official');
|
|
916
|
-
officialModules.push(entry);
|
|
917
|
-
if (entry.selected) {
|
|
918
|
-
initialValues.push(mod.code);
|
|
919
|
-
}
|
|
652
|
+
for (const mod of registryModules) {
|
|
653
|
+
const entry = await buildModuleEntry(mod);
|
|
654
|
+
allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
|
|
655
|
+
if (entry.selected) {
|
|
656
|
+
initialValues.push(mod.code);
|
|
920
657
|
}
|
|
921
658
|
}
|
|
922
|
-
allOptions.push(...officialModules.map(({ label, value, hint }) => ({ label, value, hint })));
|
|
923
|
-
|
|
924
|
-
// Group 3: Community Modules (type: community)
|
|
925
|
-
const communityModules = [];
|
|
926
|
-
for (const mod of externalModules) {
|
|
927
|
-
if (mod.type === 'community') {
|
|
928
|
-
const entry = buildModuleEntry(mod, mod.code, 'Community');
|
|
929
|
-
communityModules.push(entry);
|
|
930
|
-
if (entry.selected) {
|
|
931
|
-
initialValues.push(mod.code);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));
|
|
936
659
|
|
|
937
660
|
const selected = await prompts.autocompleteMultiselect({
|
|
938
|
-
message: 'Select modules to install:',
|
|
661
|
+
message: 'Select official modules to install:',
|
|
939
662
|
options: allOptions,
|
|
940
663
|
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
|
941
664
|
lockedValues,
|
|
@@ -945,34 +668,468 @@ class UI {
|
|
|
945
668
|
|
|
946
669
|
const result = selected ? [...selected] : [];
|
|
947
670
|
|
|
948
|
-
// Display selected modules as bulleted list
|
|
949
671
|
if (result.length > 0) {
|
|
950
672
|
const moduleLines = result.map((moduleId) => {
|
|
951
673
|
const opt = allOptions.find((o) => o.value === moduleId);
|
|
952
674
|
return ` \u2022 ${opt?.label || moduleId}`;
|
|
953
675
|
});
|
|
954
|
-
await prompts.log.message('Selected modules:\n' + moduleLines.join('\n'));
|
|
676
|
+
await prompts.log.message('Selected official modules:\n' + moduleLines.join('\n'));
|
|
955
677
|
}
|
|
956
678
|
|
|
957
679
|
return result;
|
|
958
680
|
}
|
|
959
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Browse and select community modules using category drill-down.
|
|
684
|
+
* Featured/promoted modules appear at the top.
|
|
685
|
+
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
686
|
+
* @returns {Object} { codes: string[], didBrowse: boolean }
|
|
687
|
+
*/
|
|
688
|
+
async _browseCommunityModules(installedModuleIds = new Set()) {
|
|
689
|
+
const browseCommunity = await prompts.confirm({
|
|
690
|
+
message: 'Would you like to browse community modules?',
|
|
691
|
+
default: false,
|
|
692
|
+
});
|
|
693
|
+
if (!browseCommunity) return { codes: [], didBrowse: false };
|
|
694
|
+
|
|
695
|
+
const { CommunityModuleManager } = require('./modules/community-manager');
|
|
696
|
+
const communityMgr = new CommunityModuleManager();
|
|
697
|
+
|
|
698
|
+
const s = await prompts.spinner();
|
|
699
|
+
s.start('Loading community module catalog...');
|
|
700
|
+
|
|
701
|
+
let categories, featured, allCommunity;
|
|
702
|
+
try {
|
|
703
|
+
[categories, featured, allCommunity] = await Promise.all([
|
|
704
|
+
communityMgr.getCategoryList(),
|
|
705
|
+
communityMgr.listFeatured(),
|
|
706
|
+
communityMgr.listAll(),
|
|
707
|
+
]);
|
|
708
|
+
s.stop(`Community catalog loaded (${allCommunity.length} modules)`);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
s.error('Failed to load community catalog');
|
|
711
|
+
await prompts.log.warn(` ${error.message}`);
|
|
712
|
+
return { codes: [], didBrowse: false };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (allCommunity.length === 0) {
|
|
716
|
+
await prompts.log.info('No community modules are currently available.');
|
|
717
|
+
return { codes: [], didBrowse: false };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const selectedCodes = new Set();
|
|
721
|
+
let browsing = true;
|
|
722
|
+
|
|
723
|
+
while (browsing) {
|
|
724
|
+
const categoryChoices = [];
|
|
725
|
+
|
|
726
|
+
// Featured section at top
|
|
727
|
+
if (featured.length > 0) {
|
|
728
|
+
categoryChoices.push({
|
|
729
|
+
value: '__featured__',
|
|
730
|
+
label: `\u2605 Featured (${featured.length} module${featured.length === 1 ? '' : 's'})`,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Categories with module counts
|
|
735
|
+
for (const cat of categories) {
|
|
736
|
+
categoryChoices.push({
|
|
737
|
+
value: cat.slug,
|
|
738
|
+
label: `${cat.name} (${cat.moduleCount} module${cat.moduleCount === 1 ? '' : 's'})`,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Special actions at bottom
|
|
743
|
+
categoryChoices.push(
|
|
744
|
+
{ value: '__all__', label: '\u25CE View all community modules' },
|
|
745
|
+
{ value: '__search__', label: '\u25CE Search by keyword' },
|
|
746
|
+
{ value: '__done__', label: '\u2713 Done browsing' },
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
const selectedCount = selectedCodes.size;
|
|
750
|
+
const categoryChoice = await prompts.select({
|
|
751
|
+
message: `Browse community modules${selectedCount > 0 ? ` (${selectedCount} selected)` : ''}:`,
|
|
752
|
+
choices: categoryChoices,
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
if (categoryChoice === '__done__') {
|
|
756
|
+
browsing = false;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
let modulesToShow;
|
|
761
|
+
switch (categoryChoice) {
|
|
762
|
+
case '__featured__': {
|
|
763
|
+
modulesToShow = featured;
|
|
764
|
+
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
case '__all__': {
|
|
768
|
+
modulesToShow = allCommunity;
|
|
769
|
+
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
case '__search__': {
|
|
773
|
+
const query = await prompts.text({
|
|
774
|
+
message: 'Search community modules:',
|
|
775
|
+
placeholder: 'e.g., design, testing, game',
|
|
776
|
+
});
|
|
777
|
+
if (!query || query.trim() === '') continue;
|
|
778
|
+
modulesToShow = await communityMgr.searchByKeyword(query.trim());
|
|
779
|
+
if (modulesToShow.length === 0) {
|
|
780
|
+
await prompts.log.warn('No matching modules found.');
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
default: {
|
|
787
|
+
modulesToShow = await communityMgr.listByCategory(categoryChoice);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Build options for autocompleteMultiselect
|
|
792
|
+
const trustBadge = (tier) => {
|
|
793
|
+
if (tier === 'bmad-certified') return '\u2713';
|
|
794
|
+
if (tier === 'community-reviewed') return '\u25CB';
|
|
795
|
+
return '\u26A0';
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const options = modulesToShow.map((mod) => {
|
|
799
|
+
const versionStr = mod.version ? ` (v${mod.version})` : '';
|
|
800
|
+
const badge = trustBadge(mod.trustTier);
|
|
801
|
+
return {
|
|
802
|
+
label: `${mod.displayName}${versionStr} [${badge}]`,
|
|
803
|
+
value: mod.code,
|
|
804
|
+
hint: mod.description,
|
|
805
|
+
};
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// Pre-check modules that are already selected or installed
|
|
809
|
+
const initialValues = modulesToShow.filter((m) => selectedCodes.has(m.code) || installedModuleIds.has(m.code)).map((m) => m.code);
|
|
810
|
+
|
|
811
|
+
const selected = await prompts.autocompleteMultiselect({
|
|
812
|
+
message: 'Select community modules:',
|
|
813
|
+
options,
|
|
814
|
+
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
|
815
|
+
required: false,
|
|
816
|
+
maxItems: Math.min(options.length, 10),
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// Update accumulated selections: sync with what user selected in this view
|
|
820
|
+
const shownCodes = new Set(modulesToShow.map((m) => m.code));
|
|
821
|
+
for (const code of shownCodes) {
|
|
822
|
+
if (selected && selected.includes(code)) {
|
|
823
|
+
selectedCodes.add(code);
|
|
824
|
+
} else {
|
|
825
|
+
selectedCodes.delete(code);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (selectedCodes.size > 0) {
|
|
831
|
+
const moduleLines = [];
|
|
832
|
+
for (const code of selectedCodes) {
|
|
833
|
+
const mod = await communityMgr.getModuleByCode(code);
|
|
834
|
+
moduleLines.push(` \u2022 ${mod?.displayName || code}`);
|
|
835
|
+
}
|
|
836
|
+
await prompts.log.message('Selected community modules:\n' + moduleLines.join('\n'));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return { codes: [...selectedCodes], didBrowse: true };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Prompt user to install modules from custom sources (Git URLs or local paths).
|
|
844
|
+
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
845
|
+
* @returns {Array} Selected custom module code strings
|
|
846
|
+
*/
|
|
847
|
+
async _addCustomUrlModules(installedModuleIds = new Set()) {
|
|
848
|
+
const addCustom = await prompts.confirm({
|
|
849
|
+
message: 'Would you like to install from a custom source (Git URL or local path)?',
|
|
850
|
+
default: false,
|
|
851
|
+
});
|
|
852
|
+
if (!addCustom) return [];
|
|
853
|
+
|
|
854
|
+
const { CustomModuleManager } = require('./modules/custom-module-manager');
|
|
855
|
+
const customMgr = new CustomModuleManager();
|
|
856
|
+
const selectedModules = [];
|
|
857
|
+
|
|
858
|
+
let addMore = true;
|
|
859
|
+
while (addMore) {
|
|
860
|
+
const sourceInput = await prompts.text({
|
|
861
|
+
message: 'Git URL or local path:',
|
|
862
|
+
placeholder: 'https://github.com/owner/repo or /path/to/module',
|
|
863
|
+
validate: (input) => {
|
|
864
|
+
if (!input || input.trim() === '') return 'Source is required';
|
|
865
|
+
const result = customMgr.parseSource(input.trim());
|
|
866
|
+
return result.isValid ? undefined : result.error;
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
const s = await prompts.spinner();
|
|
871
|
+
s.start('Resolving source...');
|
|
872
|
+
|
|
873
|
+
let sourceResult;
|
|
874
|
+
try {
|
|
875
|
+
sourceResult = await customMgr.resolveSource(sourceInput.trim(), { skipInstall: true, silent: true });
|
|
876
|
+
s.stop(sourceResult.parsed.type === 'local' ? 'Local source resolved' : 'Repository cloned');
|
|
877
|
+
} catch (error) {
|
|
878
|
+
s.error('Failed to resolve source');
|
|
879
|
+
await prompts.log.error(` ${error.message}`);
|
|
880
|
+
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (sourceResult.parsed.type === 'local') {
|
|
885
|
+
await prompts.log.info('LOCAL MODULE: Pointing directly at local source (changes take effect on reinstall).');
|
|
886
|
+
} else {
|
|
887
|
+
await prompts.log.warn(
|
|
888
|
+
'UNVERIFIED MODULE: This module has not been reviewed by the BMad team.\n' + ' Only install modules from sources you trust.',
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Resolve plugins based on discovery mode vs direct mode
|
|
893
|
+
s.start('Analyzing plugin structure...');
|
|
894
|
+
const allResolved = [];
|
|
895
|
+
const localPath = sourceResult.parsed.type === 'local' ? sourceResult.rootDir : null;
|
|
896
|
+
|
|
897
|
+
if (sourceResult.mode === 'discovery') {
|
|
898
|
+
// Discovery mode: marketplace.json found, list available plugins
|
|
899
|
+
let plugins;
|
|
900
|
+
try {
|
|
901
|
+
plugins = await customMgr.discoverModules(sourceResult.marketplace, sourceResult.sourceUrl);
|
|
902
|
+
} catch (discoverError) {
|
|
903
|
+
s.error('Failed to discover modules');
|
|
904
|
+
await prompts.log.error(` ${discoverError.message}`);
|
|
905
|
+
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const effectiveRepoPath = sourceResult.repoPath || sourceResult.rootDir;
|
|
910
|
+
for (const plugin of plugins) {
|
|
911
|
+
try {
|
|
912
|
+
const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath);
|
|
913
|
+
if (resolved.length > 0) {
|
|
914
|
+
allResolved.push(...resolved);
|
|
915
|
+
} else {
|
|
916
|
+
// No skills array or empty - use plugin metadata as-is (legacy)
|
|
917
|
+
allResolved.push({
|
|
918
|
+
code: plugin.code,
|
|
919
|
+
name: plugin.displayName || plugin.name,
|
|
920
|
+
version: plugin.version,
|
|
921
|
+
description: plugin.description,
|
|
922
|
+
strategy: 0,
|
|
923
|
+
pluginName: plugin.name,
|
|
924
|
+
skillPaths: [],
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
} catch (resolveError) {
|
|
928
|
+
await prompts.log.warn(` Could not resolve ${plugin.name}: ${resolveError.message}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
// Direct mode: no marketplace.json, scan directory for skills and resolve
|
|
933
|
+
const directPlugin = {
|
|
934
|
+
name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir),
|
|
935
|
+
source: '.',
|
|
936
|
+
skills: [],
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// Scan for SKILL.md directories to populate skills array
|
|
940
|
+
try {
|
|
941
|
+
const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true });
|
|
942
|
+
for (const entry of entries) {
|
|
943
|
+
if (entry.isDirectory()) {
|
|
944
|
+
const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md');
|
|
945
|
+
if (await fs.pathExists(skillMd)) {
|
|
946
|
+
directPlugin.skills.push(entry.name);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
} catch (scanError) {
|
|
951
|
+
s.error('Failed to scan directory');
|
|
952
|
+
await prompts.log.error(` ${scanError.message}`);
|
|
953
|
+
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (directPlugin.skills.length > 0) {
|
|
958
|
+
try {
|
|
959
|
+
const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath);
|
|
960
|
+
allResolved.push(...resolved);
|
|
961
|
+
} catch (resolveError) {
|
|
962
|
+
await prompts.log.warn(` Could not resolve: ${resolveError.message}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
s.stop(`Found ${allResolved.length} installable module${allResolved.length === 1 ? '' : 's'}`);
|
|
967
|
+
|
|
968
|
+
if (allResolved.length === 0) {
|
|
969
|
+
await prompts.log.warn('No installable modules found in this source.');
|
|
970
|
+
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Build multiselect choices
|
|
975
|
+
// Already-installed modules are pre-checked (update). New modules are unchecked (opt-in).
|
|
976
|
+
// Unchecking an installed module means "skip update" - removal is handled elsewhere.
|
|
977
|
+
const choices = allResolved.map((mod) => {
|
|
978
|
+
const versionStr = mod.version ? ` v${mod.version}` : '';
|
|
979
|
+
const skillCount = mod.skillPaths ? mod.skillPaths.length : 0;
|
|
980
|
+
const skillStr = skillCount > 0 ? ` (${skillCount} skill${skillCount === 1 ? '' : 's'})` : '';
|
|
981
|
+
const alreadyInstalled = installedModuleIds.has(mod.code);
|
|
982
|
+
const hint = alreadyInstalled ? 'update' : undefined;
|
|
983
|
+
|
|
984
|
+
return {
|
|
985
|
+
name: `${mod.name}${versionStr}${skillStr}`,
|
|
986
|
+
value: mod.code,
|
|
987
|
+
hint,
|
|
988
|
+
checked: alreadyInstalled,
|
|
989
|
+
};
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
// Show descriptions before the multiselect
|
|
993
|
+
for (const mod of allResolved) {
|
|
994
|
+
const versionStr = mod.version ? ` v${mod.version}` : '';
|
|
995
|
+
await prompts.log.info(` ${mod.name}${versionStr}\n ${mod.description}`);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const selected = await prompts.multiselect({
|
|
999
|
+
message: 'Select modules to install:',
|
|
1000
|
+
choices,
|
|
1001
|
+
required: false,
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
if (selected && selected.length > 0) {
|
|
1005
|
+
for (const code of selected) {
|
|
1006
|
+
selectedModules.push(code);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
addMore = await prompts.confirm({
|
|
1011
|
+
message: 'Add another custom source?',
|
|
1012
|
+
default: false,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (selectedModules.length > 0) {
|
|
1017
|
+
await prompts.log.message('Selected custom modules:\n' + selectedModules.map((c) => ` \u2022 ${c}`).join('\n'));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return selectedModules;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Resolve custom sources from --custom-source CLI flag (non-interactive).
|
|
1025
|
+
* Auto-selects all discovered modules from each source.
|
|
1026
|
+
* @param {string} sourcesArg - Comma-separated Git URLs or local paths
|
|
1027
|
+
* @returns {Array} Module codes from all resolved sources
|
|
1028
|
+
*/
|
|
1029
|
+
async _resolveCustomSourcesCli(sourcesArg) {
|
|
1030
|
+
const { CustomModuleManager } = require('./modules/custom-module-manager');
|
|
1031
|
+
const customMgr = new CustomModuleManager();
|
|
1032
|
+
const allCodes = [];
|
|
1033
|
+
|
|
1034
|
+
const sources = sourcesArg
|
|
1035
|
+
.split(',')
|
|
1036
|
+
.map((s) => s.trim())
|
|
1037
|
+
.filter(Boolean);
|
|
1038
|
+
|
|
1039
|
+
for (const source of sources) {
|
|
1040
|
+
const s = await prompts.spinner();
|
|
1041
|
+
s.start(`Resolving ${source}...`);
|
|
1042
|
+
|
|
1043
|
+
let sourceResult;
|
|
1044
|
+
try {
|
|
1045
|
+
sourceResult = await customMgr.resolveSource(source, { skipInstall: true, silent: true });
|
|
1046
|
+
s.stop(sourceResult.parsed.type === 'local' ? 'Local source resolved' : 'Repository cloned');
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
s.error(`Failed to resolve ${source}`);
|
|
1049
|
+
await prompts.log.error(` ${error.message}`);
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const s2 = await prompts.spinner();
|
|
1054
|
+
s2.start('Analyzing plugin structure...');
|
|
1055
|
+
const allResolved = [];
|
|
1056
|
+
const localPath = sourceResult.parsed.type === 'local' ? sourceResult.rootDir : null;
|
|
1057
|
+
|
|
1058
|
+
if (sourceResult.mode === 'discovery') {
|
|
1059
|
+
try {
|
|
1060
|
+
const plugins = await customMgr.discoverModules(sourceResult.marketplace, sourceResult.sourceUrl);
|
|
1061
|
+
const effectiveRepoPath = sourceResult.repoPath || sourceResult.rootDir;
|
|
1062
|
+
for (const plugin of plugins) {
|
|
1063
|
+
try {
|
|
1064
|
+
const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath);
|
|
1065
|
+
if (resolved.length > 0) {
|
|
1066
|
+
allResolved.push(...resolved);
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
// Skip unresolvable plugins
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
} catch (discoverError) {
|
|
1073
|
+
s2.error('Failed to discover modules');
|
|
1074
|
+
await prompts.log.error(` ${discoverError.message}`);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
} else {
|
|
1078
|
+
// Direct mode: scan for SKILL.md directories
|
|
1079
|
+
const directPlugin = {
|
|
1080
|
+
name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir),
|
|
1081
|
+
source: '.',
|
|
1082
|
+
skills: [],
|
|
1083
|
+
};
|
|
1084
|
+
try {
|
|
1085
|
+
const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true });
|
|
1086
|
+
for (const entry of entries) {
|
|
1087
|
+
if (entry.isDirectory()) {
|
|
1088
|
+
const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md');
|
|
1089
|
+
if (await fs.pathExists(skillMd)) {
|
|
1090
|
+
directPlugin.skills.push(entry.name);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
} catch {
|
|
1095
|
+
// Skip unreadable directories
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (directPlugin.skills.length > 0) {
|
|
1099
|
+
try {
|
|
1100
|
+
const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath);
|
|
1101
|
+
allResolved.push(...resolved);
|
|
1102
|
+
} catch {
|
|
1103
|
+
// Skip unresolvable
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
s2.stop(`Found ${allResolved.length} module${allResolved.length === 1 ? '' : 's'}`);
|
|
1108
|
+
|
|
1109
|
+
for (const mod of allResolved) {
|
|
1110
|
+
allCodes.push(mod.code);
|
|
1111
|
+
const versionStr = mod.version ? ` v${mod.version}` : '';
|
|
1112
|
+
await prompts.log.info(` Custom module: ${mod.name}${versionStr}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return allCodes;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
960
1119
|
/**
|
|
961
1120
|
* Get default modules for non-interactive mode
|
|
962
1121
|
* @param {Set} installedModuleIds - Already installed module IDs
|
|
963
1122
|
* @returns {Array} Default module codes
|
|
964
1123
|
*/
|
|
965
1124
|
async getDefaultModules(installedModuleIds = new Set()) {
|
|
966
|
-
const
|
|
967
|
-
const
|
|
968
|
-
const { modules: localModules } = await officialModules.listAvailable();
|
|
1125
|
+
const externalManager = new ExternalModuleManager();
|
|
1126
|
+
const registryModules = await externalManager.listAvailable();
|
|
969
1127
|
|
|
970
1128
|
const defaultModules = [];
|
|
971
1129
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
defaultModules.push(mod.id);
|
|
1130
|
+
for (const mod of registryModules) {
|
|
1131
|
+
if (mod.defaultSelected || installedModuleIds.has(mod.code)) {
|
|
1132
|
+
defaultModules.push(mod.code);
|
|
976
1133
|
}
|
|
977
1134
|
}
|
|
978
1135
|
|
|
@@ -1273,282 +1430,6 @@ class UI {
|
|
|
1273
1430
|
return existingInstall.ides;
|
|
1274
1431
|
}
|
|
1275
1432
|
|
|
1276
|
-
/**
|
|
1277
|
-
* Validate custom content path synchronously
|
|
1278
|
-
* @param {string} input - User input path
|
|
1279
|
-
* @returns {string|undefined} Error message or undefined if valid
|
|
1280
|
-
*/
|
|
1281
|
-
validateCustomContentPathSync(input) {
|
|
1282
|
-
// Allow empty input to cancel
|
|
1283
|
-
if (!input || input.trim() === '') {
|
|
1284
|
-
return; // Allow empty to exit
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
try {
|
|
1288
|
-
// Expand the path
|
|
1289
|
-
const expandedPath = this.expandUserPath(input.trim());
|
|
1290
|
-
|
|
1291
|
-
// Check if path exists
|
|
1292
|
-
if (!fs.pathExistsSync(expandedPath)) {
|
|
1293
|
-
return 'Path does not exist';
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// Check if it's a directory
|
|
1297
|
-
const stat = fs.statSync(expandedPath);
|
|
1298
|
-
if (!stat.isDirectory()) {
|
|
1299
|
-
return 'Path must be a directory';
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
// Check for module.yaml in the root
|
|
1303
|
-
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
1304
|
-
if (!fs.pathExistsSync(moduleYamlPath)) {
|
|
1305
|
-
return 'Directory must contain a module.yaml file in the root';
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
// Try to parse the module.yaml to get the module ID
|
|
1309
|
-
try {
|
|
1310
|
-
const yaml = require('yaml');
|
|
1311
|
-
const content = fs.readFileSync(moduleYamlPath, 'utf8');
|
|
1312
|
-
const moduleData = yaml.parse(content);
|
|
1313
|
-
if (!moduleData.code) {
|
|
1314
|
-
return 'module.yaml must contain a "code" field for the module ID';
|
|
1315
|
-
}
|
|
1316
|
-
} catch (error) {
|
|
1317
|
-
return 'Invalid module.yaml file: ' + error.message;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
return; // Valid
|
|
1321
|
-
} catch (error) {
|
|
1322
|
-
return 'Error validating path: ' + error.message;
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
/**
|
|
1327
|
-
* Prompt user for custom content source location
|
|
1328
|
-
* @returns {Object} Custom content configuration
|
|
1329
|
-
*/
|
|
1330
|
-
async promptCustomContentSource() {
|
|
1331
|
-
const customContentConfig = { hasCustomContent: true, sources: [] };
|
|
1332
|
-
|
|
1333
|
-
// Keep asking for more sources until user is done
|
|
1334
|
-
while (true) {
|
|
1335
|
-
// First ask if user wants to add another module or continue
|
|
1336
|
-
if (customContentConfig.sources.length > 0) {
|
|
1337
|
-
const action = await prompts.select({
|
|
1338
|
-
message: 'Would you like to:',
|
|
1339
|
-
choices: [
|
|
1340
|
-
{ name: 'Add another custom module', value: 'add' },
|
|
1341
|
-
{ name: 'Continue with installation', value: 'continue' },
|
|
1342
|
-
],
|
|
1343
|
-
default: 'continue',
|
|
1344
|
-
});
|
|
1345
|
-
|
|
1346
|
-
if (action === 'continue') {
|
|
1347
|
-
break;
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
let sourcePath;
|
|
1352
|
-
let isValid = false;
|
|
1353
|
-
|
|
1354
|
-
while (!isValid) {
|
|
1355
|
-
// Use sync validation because @clack/prompts doesn't support async validate
|
|
1356
|
-
const inputPath = await prompts.text({
|
|
1357
|
-
message: 'Path to custom module folder (press Enter to skip):',
|
|
1358
|
-
validate: (input) => this.validateCustomContentPathSync(input),
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
// If user pressed Enter without typing anything, exit the loop
|
|
1362
|
-
if (!inputPath || inputPath.trim() === '') {
|
|
1363
|
-
// If we have no modules yet, return false for no custom content
|
|
1364
|
-
if (customContentConfig.sources.length === 0) {
|
|
1365
|
-
return { hasCustomContent: false };
|
|
1366
|
-
}
|
|
1367
|
-
return customContentConfig;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
sourcePath = this.expandUserPath(inputPath);
|
|
1371
|
-
isValid = true;
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
// Read module.yaml to get module info
|
|
1375
|
-
const yaml = require('yaml');
|
|
1376
|
-
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
|
|
1377
|
-
const moduleContent = await fs.readFile(moduleYamlPath, 'utf8');
|
|
1378
|
-
const moduleData = yaml.parse(moduleContent);
|
|
1379
|
-
|
|
1380
|
-
// Add to sources
|
|
1381
|
-
customContentConfig.sources.push({
|
|
1382
|
-
path: sourcePath,
|
|
1383
|
-
id: moduleData.code,
|
|
1384
|
-
name: moduleData.name || moduleData.code,
|
|
1385
|
-
});
|
|
1386
|
-
|
|
1387
|
-
await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`);
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// Ask if user wants to add these to the installation
|
|
1391
|
-
const shouldInstall = await prompts.confirm({
|
|
1392
|
-
message: `Install these ${customContentConfig.sources.length} custom modules?`,
|
|
1393
|
-
default: true,
|
|
1394
|
-
});
|
|
1395
|
-
|
|
1396
|
-
if (shouldInstall) {
|
|
1397
|
-
customContentConfig.selected = true;
|
|
1398
|
-
// Store paths to module.yaml files, not directories
|
|
1399
|
-
customContentConfig.selectedFiles = customContentConfig.sources.map((s) => path.join(s.path, 'module.yaml'));
|
|
1400
|
-
// Also include module IDs for installation
|
|
1401
|
-
customContentConfig.selectedModuleIds = customContentConfig.sources.map((s) => s.id);
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
return customContentConfig;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
/**
|
|
1408
|
-
* Handle custom modules in the modify flow
|
|
1409
|
-
* @param {string} directory - Installation directory
|
|
1410
|
-
* @param {Array} selectedModules - Currently selected modules
|
|
1411
|
-
* @returns {Object} Result with selected custom modules and custom content config
|
|
1412
|
-
*/
|
|
1413
|
-
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
|
1414
|
-
// Get existing installation to find custom modules
|
|
1415
|
-
const { existingInstall } = await this.getExistingInstallation(directory);
|
|
1416
|
-
|
|
1417
|
-
// Check if there are any custom modules in cache
|
|
1418
|
-
const { Installer } = require('./core/installer');
|
|
1419
|
-
const installer = new Installer();
|
|
1420
|
-
const { bmadDir } = await installer.findBmadDir(directory);
|
|
1421
|
-
|
|
1422
|
-
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
1423
|
-
const cachedCustomModules = [];
|
|
1424
|
-
|
|
1425
|
-
if (await fs.pathExists(cacheDir)) {
|
|
1426
|
-
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
1427
|
-
for (const entry of entries) {
|
|
1428
|
-
if (entry.isDirectory()) {
|
|
1429
|
-
const moduleYamlPath = path.join(cacheDir, entry.name, 'module.yaml');
|
|
1430
|
-
if (await fs.pathExists(moduleYamlPath)) {
|
|
1431
|
-
const yaml = require('yaml');
|
|
1432
|
-
const content = await fs.readFile(moduleYamlPath, 'utf8');
|
|
1433
|
-
const moduleData = yaml.parse(content);
|
|
1434
|
-
|
|
1435
|
-
cachedCustomModules.push({
|
|
1436
|
-
id: entry.name,
|
|
1437
|
-
name: moduleData.name || entry.name,
|
|
1438
|
-
description: moduleData.description || 'Custom module from cache',
|
|
1439
|
-
checked: selectedModules.includes(entry.name),
|
|
1440
|
-
fromCache: true,
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
const result = {
|
|
1448
|
-
selectedCustomModules: [],
|
|
1449
|
-
customContentConfig: { hasCustomContent: false },
|
|
1450
|
-
};
|
|
1451
|
-
|
|
1452
|
-
// Ask user about custom modules
|
|
1453
|
-
await prompts.log.info('Custom Modules');
|
|
1454
|
-
if (cachedCustomModules.length > 0) {
|
|
1455
|
-
await prompts.log.message('Found custom modules in your installation:');
|
|
1456
|
-
} else {
|
|
1457
|
-
await prompts.log.message('No custom modules currently installed.');
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
// Build choices dynamically based on whether we have existing modules
|
|
1461
|
-
const choices = [];
|
|
1462
|
-
if (cachedCustomModules.length > 0) {
|
|
1463
|
-
choices.push(
|
|
1464
|
-
{ name: 'Keep all existing custom modules', value: 'keep' },
|
|
1465
|
-
{ name: 'Select which custom modules to keep', value: 'select' },
|
|
1466
|
-
{ name: 'Add new custom modules', value: 'add' },
|
|
1467
|
-
{ name: 'Remove all custom modules', value: 'remove' },
|
|
1468
|
-
);
|
|
1469
|
-
} else {
|
|
1470
|
-
choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
const customAction = await prompts.select({
|
|
1474
|
-
message: cachedCustomModules.length > 0 ? 'Manage custom modules?' : 'Add custom modules?',
|
|
1475
|
-
choices: choices,
|
|
1476
|
-
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
|
|
1477
|
-
});
|
|
1478
|
-
|
|
1479
|
-
switch (customAction) {
|
|
1480
|
-
case 'keep': {
|
|
1481
|
-
// Keep all existing custom modules
|
|
1482
|
-
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
|
|
1483
|
-
await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`);
|
|
1484
|
-
break;
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
case 'select': {
|
|
1488
|
-
// Let user choose which to keep
|
|
1489
|
-
const selectChoices = cachedCustomModules.map((m) => ({
|
|
1490
|
-
name: `${m.name} (${m.id})`,
|
|
1491
|
-
value: m.id,
|
|
1492
|
-
checked: m.checked,
|
|
1493
|
-
}));
|
|
1494
|
-
|
|
1495
|
-
// Add "None / I changed my mind" option at the end
|
|
1496
|
-
const choicesWithSkip = [
|
|
1497
|
-
...selectChoices,
|
|
1498
|
-
{
|
|
1499
|
-
name: '⚠ None / I changed my mind - keep no custom modules',
|
|
1500
|
-
value: '__NONE__',
|
|
1501
|
-
checked: false,
|
|
1502
|
-
},
|
|
1503
|
-
];
|
|
1504
|
-
|
|
1505
|
-
const keepModules = await prompts.multiselect({
|
|
1506
|
-
message: 'Select custom modules to keep (use arrow keys, space to toggle):',
|
|
1507
|
-
choices: choicesWithSkip,
|
|
1508
|
-
required: true,
|
|
1509
|
-
});
|
|
1510
|
-
|
|
1511
|
-
// If user selected both "__NONE__" and other modules, honor the "None" choice
|
|
1512
|
-
if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) {
|
|
1513
|
-
await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.');
|
|
1514
|
-
result.selectedCustomModules = [];
|
|
1515
|
-
} else {
|
|
1516
|
-
// Filter out the special '__NONE__' value
|
|
1517
|
-
result.selectedCustomModules = keepModules ? keepModules.filter((m) => m !== '__NONE__') : [];
|
|
1518
|
-
}
|
|
1519
|
-
break;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
case 'add': {
|
|
1523
|
-
// By default, keep existing modules when adding new ones
|
|
1524
|
-
// User chose "Add new" not "Replace", so we assume they want to keep existing
|
|
1525
|
-
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
|
|
1526
|
-
|
|
1527
|
-
// Then prompt for new ones (reuse existing method)
|
|
1528
|
-
const newCustomContent = await this.promptCustomContentSource();
|
|
1529
|
-
if (newCustomContent.hasCustomContent && newCustomContent.selected) {
|
|
1530
|
-
result.selectedCustomModules.push(...newCustomContent.selectedModuleIds);
|
|
1531
|
-
result.customContentConfig = newCustomContent;
|
|
1532
|
-
}
|
|
1533
|
-
break;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
case 'remove': {
|
|
1537
|
-
// Remove all custom modules
|
|
1538
|
-
await prompts.log.warn('All custom modules will be removed from the installation');
|
|
1539
|
-
break;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
case 'cancel': {
|
|
1543
|
-
// User cancelled - no custom modules
|
|
1544
|
-
await prompts.log.message('No custom modules will be added');
|
|
1545
|
-
break;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
return result;
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
1433
|
/**
|
|
1553
1434
|
* Display module versions with update availability
|
|
1554
1435
|
* @param {Array} modules - Array of module info objects with version info
|
|
@@ -1558,6 +1439,7 @@ class UI {
|
|
|
1558
1439
|
// Group modules by source
|
|
1559
1440
|
const builtIn = modules.filter((m) => m.source === 'built-in');
|
|
1560
1441
|
const external = modules.filter((m) => m.source === 'external');
|
|
1442
|
+
const community = modules.filter((m) => m.source === 'community');
|
|
1561
1443
|
const custom = modules.filter((m) => m.source === 'custom');
|
|
1562
1444
|
const unknown = modules.filter((m) => m.source === 'unknown');
|
|
1563
1445
|
|
|
@@ -1578,6 +1460,7 @@ class UI {
|
|
|
1578
1460
|
|
|
1579
1461
|
formatGroup(builtIn, 'Built-in Modules');
|
|
1580
1462
|
formatGroup(external, 'External Modules (Official)');
|
|
1463
|
+
formatGroup(community, 'Community Modules');
|
|
1581
1464
|
formatGroup(custom, 'Custom Modules');
|
|
1582
1465
|
formatGroup(unknown, 'Other Modules');
|
|
1583
1466
|
|