bmad-method 6.2.3-next.25 → 6.2.3-next.26

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.
@@ -2,7 +2,6 @@ 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');
7
6
  const { getProjectRoot } = require('./project-root');
8
7
  const prompts = require('./prompts');
@@ -48,19 +47,6 @@ function _extractMarketplaceVersion(data) {
48
47
  return best;
49
48
  }
50
49
 
51
- // Separator class for visual grouping in select/multiselect prompts
52
- // Note: @clack/prompts doesn't support separators natively, they are filtered out
53
- class Separator {
54
- constructor(text = '────────') {
55
- this.line = text;
56
- this.name = text;
57
- }
58
- type = 'separator';
59
- }
60
-
61
- // Separator for choice lists (compatible interface)
62
- const choiceUtils = { Separator };
63
-
64
50
  /**
65
51
  * UI utilities for the installer
66
52
  */
@@ -100,11 +86,6 @@ class UI {
100
86
  // Check if there's an existing BMAD installation
101
87
  const hasExistingInstall = await fs.pathExists(bmadDir);
102
88
 
103
- let customContentConfig = { hasCustomContent: false };
104
- if (!hasExistingInstall) {
105
- customContentConfig._shouldAsk = true;
106
- }
107
-
108
89
  // Track action type (only set if there's an existing installation)
109
90
  let actionType;
110
91
 
@@ -153,48 +134,9 @@ class UI {
153
134
 
154
135
  // Handle quick update separately
155
136
  if (actionType === 'quick-update') {
156
- // Pass --custom-content through so installer can re-cache if cache is missing
157
- let customContentForQuickUpdate = { hasCustomContent: false };
158
- if (options.customContent) {
159
- const paths = options.customContent
160
- .split(',')
161
- .map((p) => p.trim())
162
- .filter(Boolean);
163
- if (paths.length > 0) {
164
- const customPaths = [];
165
- const selectedModuleIds = [];
166
- const sources = [];
167
- for (const customPath of paths) {
168
- const expandedPath = this.expandUserPath(customPath);
169
- const validation = this.validateCustomContentPathSync(expandedPath);
170
- if (validation) continue;
171
- let moduleMeta;
172
- try {
173
- const moduleYamlPath = path.join(expandedPath, 'module.yaml');
174
- moduleMeta = require('yaml').parse(await fs.readFile(moduleYamlPath, 'utf-8'));
175
- } catch {
176
- continue;
177
- }
178
- if (!moduleMeta?.code) continue;
179
- customPaths.push(expandedPath);
180
- selectedModuleIds.push(moduleMeta.code);
181
- sources.push({ path: expandedPath, id: moduleMeta.code, name: moduleMeta.name || moduleMeta.code });
182
- }
183
- if (customPaths.length > 0) {
184
- customContentForQuickUpdate = {
185
- hasCustomContent: true,
186
- selected: true,
187
- sources,
188
- selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
189
- selectedModuleIds,
190
- };
191
- }
192
- }
193
- }
194
137
  return {
195
138
  actionType: 'quick-update',
196
139
  directory: confirmedDirectory,
197
- customContent: customContentForQuickUpdate,
198
140
  skipPrompts: options.yes || false,
199
141
  };
200
142
  }
@@ -225,120 +167,6 @@ class UI {
225
167
  selectedModules = await this.selectAllModules(installedModuleIds);
226
168
  }
227
169
 
228
- // After module selection, ask about custom modules
229
- let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
230
-
231
- if (options.customContent) {
232
- // Use custom content from command-line
233
- const paths = options.customContent
234
- .split(',')
235
- .map((p) => p.trim())
236
- .filter(Boolean);
237
- await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
238
-
239
- // Build custom content config similar to promptCustomContentSource
240
- const customPaths = [];
241
- const selectedModuleIds = [];
242
- const sources = [];
243
-
244
- for (const customPath of paths) {
245
- const expandedPath = this.expandUserPath(customPath);
246
- const validation = this.validateCustomContentPathSync(expandedPath);
247
- if (validation) {
248
- await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
249
- continue;
250
- }
251
-
252
- // Read module metadata
253
- let moduleMeta;
254
- try {
255
- const moduleYamlPath = path.join(expandedPath, 'module.yaml');
256
- const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
257
- const yaml = require('yaml');
258
- moduleMeta = yaml.parse(moduleYaml);
259
- } catch (error) {
260
- await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
261
- continue;
262
- }
263
-
264
- if (!moduleMeta) {
265
- await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
266
- continue;
267
- }
268
-
269
- if (!moduleMeta.code) {
270
- await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
271
- continue;
272
- }
273
-
274
- customPaths.push(expandedPath);
275
- selectedModuleIds.push(moduleMeta.code);
276
- sources.push({
277
- path: expandedPath,
278
- id: moduleMeta.code,
279
- name: moduleMeta.name || moduleMeta.code,
280
- });
281
- }
282
-
283
- if (customPaths.length > 0) {
284
- customModuleResult = {
285
- selectedCustomModules: selectedModuleIds,
286
- customContentConfig: {
287
- hasCustomContent: true,
288
- selected: true,
289
- sources,
290
- selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
291
- selectedModuleIds: selectedModuleIds,
292
- },
293
- };
294
- }
295
- } else if (options.yes) {
296
- // Non-interactive mode: preserve existing custom modules (matches default: false)
297
- const cacheDir = path.join(bmadDir, '_config', 'custom');
298
- if (await fs.pathExists(cacheDir)) {
299
- const entries = await fs.readdir(cacheDir, { withFileTypes: true });
300
- for (const entry of entries) {
301
- if (entry.isDirectory()) {
302
- customModuleResult.selectedCustomModules.push(entry.name);
303
- }
304
- }
305
- await prompts.log.info(
306
- `Non-interactive mode (--yes): preserving ${customModuleResult.selectedCustomModules.length} existing custom module(s)`,
307
- );
308
- } else {
309
- await prompts.log.info('Non-interactive mode (--yes): no existing custom modules found');
310
- }
311
- } else {
312
- const changeCustomModules = await prompts.confirm({
313
- message: 'Modify custom modules, agents, or workflows?',
314
- default: false,
315
- });
316
-
317
- if (changeCustomModules) {
318
- customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
319
- } else {
320
- // Preserve existing custom modules if user doesn't want to modify them
321
- const { Installer } = require('./core/installer');
322
- const installer = new Installer();
323
- const { bmadDir } = await installer.findBmadDir(confirmedDirectory);
324
-
325
- const cacheDir = path.join(bmadDir, '_config', 'custom');
326
- if (await fs.pathExists(cacheDir)) {
327
- const entries = await fs.readdir(cacheDir, { withFileTypes: true });
328
- for (const entry of entries) {
329
- if (entry.isDirectory()) {
330
- customModuleResult.selectedCustomModules.push(entry.name);
331
- }
332
- }
333
- }
334
- }
335
- }
336
-
337
- // Merge any selected custom modules
338
- if (customModuleResult.selectedCustomModules.length > 0) {
339
- selectedModules.push(...customModuleResult.selectedCustomModules);
340
- }
341
-
342
170
  // Ensure core is in the modules list
343
171
  if (!selectedModules.includes('core')) {
344
172
  selectedModules.unshift('core');
@@ -357,7 +185,6 @@ class UI {
357
185
  skipIde: toolSelection.skipIde,
358
186
  coreConfig: moduleConfigs.core || {},
359
187
  moduleConfigs: moduleConfigs,
360
- customContent: customModuleResult.customContentConfig,
361
188
  skipPrompts: options.yes || false,
362
189
  };
363
190
  }
@@ -383,84 +210,6 @@ class UI {
383
210
  selectedModules = await this.selectAllModules(installedModuleIds);
384
211
  }
385
212
 
386
- // Ask about custom content (local modules/agents/workflows)
387
- if (options.customContent) {
388
- // Use custom content from command-line
389
- const paths = options.customContent
390
- .split(',')
391
- .map((p) => p.trim())
392
- .filter(Boolean);
393
- await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
394
-
395
- // Build custom content config similar to promptCustomContentSource
396
- const customPaths = [];
397
- const selectedModuleIds = [];
398
- const sources = [];
399
-
400
- for (const customPath of paths) {
401
- const expandedPath = this.expandUserPath(customPath);
402
- const validation = this.validateCustomContentPathSync(expandedPath);
403
- if (validation) {
404
- await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
405
- continue;
406
- }
407
-
408
- // Read module metadata
409
- let moduleMeta;
410
- try {
411
- const moduleYamlPath = path.join(expandedPath, 'module.yaml');
412
- const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
413
- const yaml = require('yaml');
414
- moduleMeta = yaml.parse(moduleYaml);
415
- } catch (error) {
416
- await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
417
- continue;
418
- }
419
-
420
- if (!moduleMeta) {
421
- await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
422
- continue;
423
- }
424
-
425
- if (!moduleMeta.code) {
426
- await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
427
- continue;
428
- }
429
-
430
- customPaths.push(expandedPath);
431
- selectedModuleIds.push(moduleMeta.code);
432
- sources.push({
433
- path: expandedPath,
434
- id: moduleMeta.code,
435
- name: moduleMeta.name || moduleMeta.code,
436
- });
437
- }
438
-
439
- if (customPaths.length > 0) {
440
- customContentConfig = {
441
- hasCustomContent: true,
442
- selected: true,
443
- sources,
444
- selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
445
- selectedModuleIds: selectedModuleIds,
446
- };
447
- }
448
- } else if (!options.yes) {
449
- const wantsCustomContent = await prompts.confirm({
450
- message: 'Add custom modules, agents, or workflows from your computer?',
451
- default: false,
452
- });
453
-
454
- if (wantsCustomContent) {
455
- customContentConfig = await this.promptCustomContentSource();
456
- }
457
- }
458
-
459
- // Add custom content modules if any were selected
460
- if (customContentConfig && customContentConfig.selectedModuleIds) {
461
- selectedModules.push(...customContentConfig.selectedModuleIds);
462
- }
463
-
464
213
  // Ensure core is in the modules list
465
214
  if (!selectedModules.includes('core')) {
466
215
  selectedModules.unshift('core');
@@ -476,7 +225,6 @@ class UI {
476
225
  skipIde: toolSelection.skipIde,
477
226
  coreConfig: moduleConfigs.core || {},
478
227
  moduleConfigs: moduleConfigs,
479
- customContent: customContentConfig,
480
228
  skipPrompts: options.yes || false,
481
229
  };
482
230
  }
@@ -814,90 +562,6 @@ class UI {
814
562
  return configCollector.collectedConfig;
815
563
  }
816
564
 
817
- /**
818
- * Get module choices for selection
819
- * @param {Set} installedModuleIds - Currently installed module IDs
820
- * @param {Object} customContentConfig - Custom content configuration
821
- * @returns {Array} Module choices for prompt
822
- */
823
- async getModuleChoices(installedModuleIds, customContentConfig = null) {
824
- const color = await prompts.getColor();
825
- const moduleChoices = [];
826
- const isNewInstallation = installedModuleIds.size === 0;
827
-
828
- const customContentItems = [];
829
-
830
- // Add custom content items
831
- if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
832
- // Existing installation - show from directory
833
- const customHandler = new CustomHandler();
834
- const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
835
-
836
- for (const customFile of customFiles) {
837
- const customInfo = await customHandler.getCustomInfo(customFile);
838
- if (customInfo) {
839
- customContentItems.push({
840
- name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`,
841
- value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
842
- checked: true, // Default to selected since user chose to provide custom content
843
- path: customInfo.path, // Track path to avoid duplicates
844
- hint: customInfo.description || undefined,
845
- });
846
- }
847
- }
848
- }
849
-
850
- // Add official modules
851
- const { OfficialModules } = require('./modules/official-modules');
852
- const officialModules = new OfficialModules();
853
- const { modules: availableModules, customModules: customModulesFromCache } = await officialModules.listAvailable();
854
-
855
- // First, add all items to appropriate sections
856
- const allCustomModules = [];
857
-
858
- // Add custom content items from directory
859
- allCustomModules.push(...customContentItems);
860
-
861
- // Add custom modules from cache
862
- for (const mod of customModulesFromCache) {
863
- // Skip if this module is already in customContentItems (by path)
864
- const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path));
865
-
866
- if (!isDuplicate) {
867
- allCustomModules.push({
868
- name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`,
869
- value: mod.id,
870
- checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
871
- hint: mod.description || undefined,
872
- });
873
- }
874
- }
875
-
876
- // Add separators and modules in correct order
877
- if (allCustomModules.length > 0) {
878
- // Add separator for custom content, all custom modules, and official content separator
879
- moduleChoices.push(
880
- new choiceUtils.Separator('── Custom Content ──'),
881
- ...allCustomModules,
882
- new choiceUtils.Separator('── Official Content ──'),
883
- );
884
- }
885
-
886
- // Add official modules (only non-custom ones)
887
- for (const mod of availableModules) {
888
- if (!mod.isCustom) {
889
- moduleChoices.push({
890
- name: mod.name,
891
- value: mod.id,
892
- checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
893
- hint: mod.description || undefined,
894
- });
895
- }
896
- }
897
-
898
- return moduleChoices;
899
- }
900
-
901
565
  /**
902
566
  * Select all modules (official + community) using grouped multiselect.
903
567
  * Core is shown as locked but filtered from the result since it's always installed separately.
@@ -941,7 +605,7 @@ class UI {
941
605
  // Local modules (BMM, BMB, etc.)
942
606
  const localEntries = [];
943
607
  for (const mod of localModules) {
944
- if (!mod.isCustom && mod.id !== 'core') {
608
+ if (mod.id !== 'core') {
945
609
  const entry = await buildModuleEntry(mod, mod.id, 'Local');
946
610
  localEntries.push(entry);
947
611
  if (entry.selected) {
@@ -1316,282 +980,6 @@ class UI {
1316
980
  return existingInstall.ides;
1317
981
  }
1318
982
 
1319
- /**
1320
- * Validate custom content path synchronously
1321
- * @param {string} input - User input path
1322
- * @returns {string|undefined} Error message or undefined if valid
1323
- */
1324
- validateCustomContentPathSync(input) {
1325
- // Allow empty input to cancel
1326
- if (!input || input.trim() === '') {
1327
- return; // Allow empty to exit
1328
- }
1329
-
1330
- try {
1331
- // Expand the path
1332
- const expandedPath = this.expandUserPath(input.trim());
1333
-
1334
- // Check if path exists
1335
- if (!fs.pathExistsSync(expandedPath)) {
1336
- return 'Path does not exist';
1337
- }
1338
-
1339
- // Check if it's a directory
1340
- const stat = fs.statSync(expandedPath);
1341
- if (!stat.isDirectory()) {
1342
- return 'Path must be a directory';
1343
- }
1344
-
1345
- // Check for module.yaml in the root
1346
- const moduleYamlPath = path.join(expandedPath, 'module.yaml');
1347
- if (!fs.pathExistsSync(moduleYamlPath)) {
1348
- return 'Directory must contain a module.yaml file in the root';
1349
- }
1350
-
1351
- // Try to parse the module.yaml to get the module ID
1352
- try {
1353
- const yaml = require('yaml');
1354
- const content = fs.readFileSync(moduleYamlPath, 'utf8');
1355
- const moduleData = yaml.parse(content);
1356
- if (!moduleData.code) {
1357
- return 'module.yaml must contain a "code" field for the module ID';
1358
- }
1359
- } catch (error) {
1360
- return 'Invalid module.yaml file: ' + error.message;
1361
- }
1362
-
1363
- return; // Valid
1364
- } catch (error) {
1365
- return 'Error validating path: ' + error.message;
1366
- }
1367
- }
1368
-
1369
- /**
1370
- * Prompt user for custom content source location
1371
- * @returns {Object} Custom content configuration
1372
- */
1373
- async promptCustomContentSource() {
1374
- const customContentConfig = { hasCustomContent: true, sources: [] };
1375
-
1376
- // Keep asking for more sources until user is done
1377
- while (true) {
1378
- // First ask if user wants to add another module or continue
1379
- if (customContentConfig.sources.length > 0) {
1380
- const action = await prompts.select({
1381
- message: 'Would you like to:',
1382
- choices: [
1383
- { name: 'Add another custom module', value: 'add' },
1384
- { name: 'Continue with installation', value: 'continue' },
1385
- ],
1386
- default: 'continue',
1387
- });
1388
-
1389
- if (action === 'continue') {
1390
- break;
1391
- }
1392
- }
1393
-
1394
- let sourcePath;
1395
- let isValid = false;
1396
-
1397
- while (!isValid) {
1398
- // Use sync validation because @clack/prompts doesn't support async validate
1399
- const inputPath = await prompts.text({
1400
- message: 'Path to custom module folder (press Enter to skip):',
1401
- validate: (input) => this.validateCustomContentPathSync(input),
1402
- });
1403
-
1404
- // If user pressed Enter without typing anything, exit the loop
1405
- if (!inputPath || inputPath.trim() === '') {
1406
- // If we have no modules yet, return false for no custom content
1407
- if (customContentConfig.sources.length === 0) {
1408
- return { hasCustomContent: false };
1409
- }
1410
- return customContentConfig;
1411
- }
1412
-
1413
- sourcePath = this.expandUserPath(inputPath);
1414
- isValid = true;
1415
- }
1416
-
1417
- // Read module.yaml to get module info
1418
- const yaml = require('yaml');
1419
- const moduleYamlPath = path.join(sourcePath, 'module.yaml');
1420
- const moduleContent = await fs.readFile(moduleYamlPath, 'utf8');
1421
- const moduleData = yaml.parse(moduleContent);
1422
-
1423
- // Add to sources
1424
- customContentConfig.sources.push({
1425
- path: sourcePath,
1426
- id: moduleData.code,
1427
- name: moduleData.name || moduleData.code,
1428
- });
1429
-
1430
- await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`);
1431
- }
1432
-
1433
- // Ask if user wants to add these to the installation
1434
- const shouldInstall = await prompts.confirm({
1435
- message: `Install these ${customContentConfig.sources.length} custom modules?`,
1436
- default: true,
1437
- });
1438
-
1439
- if (shouldInstall) {
1440
- customContentConfig.selected = true;
1441
- // Store paths to module.yaml files, not directories
1442
- customContentConfig.selectedFiles = customContentConfig.sources.map((s) => path.join(s.path, 'module.yaml'));
1443
- // Also include module IDs for installation
1444
- customContentConfig.selectedModuleIds = customContentConfig.sources.map((s) => s.id);
1445
- }
1446
-
1447
- return customContentConfig;
1448
- }
1449
-
1450
- /**
1451
- * Handle custom modules in the modify flow
1452
- * @param {string} directory - Installation directory
1453
- * @param {Array} selectedModules - Currently selected modules
1454
- * @returns {Object} Result with selected custom modules and custom content config
1455
- */
1456
- async handleCustomModulesInModifyFlow(directory, selectedModules) {
1457
- // Get existing installation to find custom modules
1458
- const { existingInstall } = await this.getExistingInstallation(directory);
1459
-
1460
- // Check if there are any custom modules in cache
1461
- const { Installer } = require('./core/installer');
1462
- const installer = new Installer();
1463
- const { bmadDir } = await installer.findBmadDir(directory);
1464
-
1465
- const cacheDir = path.join(bmadDir, '_config', 'custom');
1466
- const cachedCustomModules = [];
1467
-
1468
- if (await fs.pathExists(cacheDir)) {
1469
- const entries = await fs.readdir(cacheDir, { withFileTypes: true });
1470
- for (const entry of entries) {
1471
- if (entry.isDirectory()) {
1472
- const moduleYamlPath = path.join(cacheDir, entry.name, 'module.yaml');
1473
- if (await fs.pathExists(moduleYamlPath)) {
1474
- const yaml = require('yaml');
1475
- const content = await fs.readFile(moduleYamlPath, 'utf8');
1476
- const moduleData = yaml.parse(content);
1477
-
1478
- cachedCustomModules.push({
1479
- id: entry.name,
1480
- name: moduleData.name || entry.name,
1481
- description: moduleData.description || 'Custom module from cache',
1482
- checked: selectedModules.includes(entry.name),
1483
- fromCache: true,
1484
- });
1485
- }
1486
- }
1487
- }
1488
- }
1489
-
1490
- const result = {
1491
- selectedCustomModules: [],
1492
- customContentConfig: { hasCustomContent: false },
1493
- };
1494
-
1495
- // Ask user about custom modules
1496
- await prompts.log.info('Custom Modules');
1497
- if (cachedCustomModules.length > 0) {
1498
- await prompts.log.message('Found custom modules in your installation:');
1499
- } else {
1500
- await prompts.log.message('No custom modules currently installed.');
1501
- }
1502
-
1503
- // Build choices dynamically based on whether we have existing modules
1504
- const choices = [];
1505
- if (cachedCustomModules.length > 0) {
1506
- choices.push(
1507
- { name: 'Keep all existing custom modules', value: 'keep' },
1508
- { name: 'Select which custom modules to keep', value: 'select' },
1509
- { name: 'Add new custom modules', value: 'add' },
1510
- { name: 'Remove all custom modules', value: 'remove' },
1511
- );
1512
- } else {
1513
- choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
1514
- }
1515
-
1516
- const customAction = await prompts.select({
1517
- message: cachedCustomModules.length > 0 ? 'Manage custom modules?' : 'Add custom modules?',
1518
- choices: choices,
1519
- default: cachedCustomModules.length > 0 ? 'keep' : 'add',
1520
- });
1521
-
1522
- switch (customAction) {
1523
- case 'keep': {
1524
- // Keep all existing custom modules
1525
- result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
1526
- await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`);
1527
- break;
1528
- }
1529
-
1530
- case 'select': {
1531
- // Let user choose which to keep
1532
- const selectChoices = cachedCustomModules.map((m) => ({
1533
- name: `${m.name} (${m.id})`,
1534
- value: m.id,
1535
- checked: m.checked,
1536
- }));
1537
-
1538
- // Add "None / I changed my mind" option at the end
1539
- const choicesWithSkip = [
1540
- ...selectChoices,
1541
- {
1542
- name: '⚠ None / I changed my mind - keep no custom modules',
1543
- value: '__NONE__',
1544
- checked: false,
1545
- },
1546
- ];
1547
-
1548
- const keepModules = await prompts.multiselect({
1549
- message: 'Select custom modules to keep (use arrow keys, space to toggle):',
1550
- choices: choicesWithSkip,
1551
- required: true,
1552
- });
1553
-
1554
- // If user selected both "__NONE__" and other modules, honor the "None" choice
1555
- if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) {
1556
- await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.');
1557
- result.selectedCustomModules = [];
1558
- } else {
1559
- // Filter out the special '__NONE__' value
1560
- result.selectedCustomModules = keepModules ? keepModules.filter((m) => m !== '__NONE__') : [];
1561
- }
1562
- break;
1563
- }
1564
-
1565
- case 'add': {
1566
- // By default, keep existing modules when adding new ones
1567
- // User chose "Add new" not "Replace", so we assume they want to keep existing
1568
- result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
1569
-
1570
- // Then prompt for new ones (reuse existing method)
1571
- const newCustomContent = await this.promptCustomContentSource();
1572
- if (newCustomContent.hasCustomContent && newCustomContent.selected) {
1573
- result.selectedCustomModules.push(...newCustomContent.selectedModuleIds);
1574
- result.customContentConfig = newCustomContent;
1575
- }
1576
- break;
1577
- }
1578
-
1579
- case 'remove': {
1580
- // Remove all custom modules
1581
- await prompts.log.warn('All custom modules will be removed from the installation');
1582
- break;
1583
- }
1584
-
1585
- case 'cancel': {
1586
- // User cancelled - no custom modules
1587
- await prompts.log.message('No custom modules will be added');
1588
- break;
1589
- }
1590
- }
1591
-
1592
- return result;
1593
- }
1594
-
1595
983
  /**
1596
984
  * Display module versions with update availability
1597
985
  * @param {Array} modules - Array of module info objects with version info