bmad-method 6.2.3-next.3 → 6.2.3-next.30

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.
Files changed (126) hide show
  1. package/.claude-plugin/marketplace.json +0 -4
  2. package/README.md +8 -9
  3. package/README_CN.md +1 -1
  4. package/README_VN.md +110 -0
  5. package/package.json +2 -1
  6. package/removals.txt +17 -0
  7. package/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md +7 -4
  8. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md +6 -4
  9. package/src/bmm-skills/1-analysis/bmad-document-project/workflow.md +8 -10
  10. package/src/bmm-skills/1-analysis/bmad-prfaq/SKILL.md +96 -0
  11. package/src/bmm-skills/1-analysis/bmad-prfaq/agents/artifact-analyzer.md +60 -0
  12. package/src/bmm-skills/1-analysis/bmad-prfaq/agents/web-researcher.md +49 -0
  13. package/src/bmm-skills/1-analysis/bmad-prfaq/assets/prfaq-template.md +62 -0
  14. package/src/bmm-skills/1-analysis/bmad-prfaq/bmad-manifest.json +16 -0
  15. package/src/bmm-skills/1-analysis/bmad-prfaq/references/customer-faq.md +55 -0
  16. package/src/bmm-skills/1-analysis/bmad-prfaq/references/internal-faq.md +51 -0
  17. package/src/bmm-skills/1-analysis/bmad-prfaq/references/press-release.md +60 -0
  18. package/src/bmm-skills/1-analysis/bmad-prfaq/references/verdict.md +79 -0
  19. package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +1 -6
  20. package/src/bmm-skills/1-analysis/bmad-product-brief/bmad-manifest.json +1 -1
  21. package/src/bmm-skills/1-analysis/research/bmad-domain-research/workflow.md +8 -6
  22. package/src/bmm-skills/1-analysis/research/bmad-market-research/workflow.md +8 -6
  23. package/src/bmm-skills/1-analysis/research/bmad-technical-research/workflow.md +8 -6
  24. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md +6 -4
  25. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md +6 -4
  26. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/workflow.md +8 -9
  27. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-13-responsive-accessibility.md +1 -1
  28. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/workflow.md +8 -9
  29. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01-discovery.md +1 -1
  30. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01b-legacy-conversion.md +1 -1
  31. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-02-review.md +1 -1
  32. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-03-edit.md +1 -1
  33. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +1 -3
  34. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/workflow.md +8 -9
  35. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/workflow.md +8 -9
  36. package/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md +6 -4
  37. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-01-document-discovery.md +1 -1
  38. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-02-prd-analysis.md +1 -1
  39. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-03-epic-coverage-validation.md +1 -1
  40. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +9 -11
  41. package/src/bmm-skills/3-solutioning/bmad-create-architecture/workflow.md +8 -14
  42. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +10 -12
  43. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/workflow.md +8 -12
  44. package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +11 -4
  45. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/SKILL.md +29 -0
  46. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/generate-trail.md +38 -0
  47. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-01-orientation.md +105 -0
  48. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-02-walkthrough.md +89 -0
  49. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-03-detail-pass.md +106 -0
  50. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-04-testing.md +74 -0
  51. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-05-wrapup.md +24 -0
  52. package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-01-gather-context.md +38 -15
  53. package/src/bmm-skills/4-implementation/bmad-correct-course/checklist.md +2 -2
  54. package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +8 -8
  55. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/checklist.md +1 -1
  56. package/src/bmm-skills/4-implementation/bmad-quick-dev/spec-template.md +1 -1
  57. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md +20 -6
  58. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-02-plan.md +20 -8
  59. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-03-implement.md +2 -0
  60. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +16 -4
  61. package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +1 -5
  62. package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +134 -134
  63. package/src/bmm-skills/4-implementation/bmad-sprint-planning/sprint-status-template.yaml +1 -1
  64. package/src/bmm-skills/4-implementation/bmad-sprint-planning/workflow.md +3 -3
  65. package/src/bmm-skills/4-implementation/bmad-sprint-status/workflow.md +2 -2
  66. package/src/bmm-skills/module-help.csv +4 -1
  67. package/src/core-skills/bmad-advanced-elicitation/SKILL.md +1 -2
  68. package/src/core-skills/bmad-distillator/SKILL.md +0 -1
  69. package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +9 -9
  70. package/src/core-skills/bmad-help/SKILL.md +4 -2
  71. package/src/core-skills/bmad-party-mode/SKILL.md +121 -2
  72. package/src/core-skills/module-help.csv +1 -0
  73. package/tools/installer/cli-utils.js +18 -9
  74. package/tools/installer/commands/install.js +0 -1
  75. package/tools/installer/core/existing-install.js +2 -8
  76. package/tools/installer/core/install-paths.js +0 -3
  77. package/tools/installer/core/installer.js +176 -464
  78. package/tools/installer/core/manifest-generator.js +4 -12
  79. package/tools/installer/core/manifest.js +82 -97
  80. package/tools/installer/ide/_config-driven.js +149 -38
  81. package/tools/installer/ide/platform-codes.yaml +6 -4
  82. package/tools/installer/ide/shared/skill-manifest.js +1 -16
  83. package/tools/installer/install-messages.yaml +19 -26
  84. package/tools/installer/modules/community-manager.js +377 -0
  85. package/tools/installer/modules/custom-module-manager.js +308 -0
  86. package/tools/installer/modules/external-manager.js +65 -49
  87. package/tools/installer/modules/official-modules.js +37 -65
  88. package/tools/installer/modules/registry-client.js +66 -0
  89. package/tools/installer/{external-official-modules.yaml → modules/registry-fallback.yaml} +3 -12
  90. package/tools/installer/ui.js +340 -672
  91. package/tools/platform-codes.yaml +6 -0
  92. package/src/bmm-skills/2-plan-workflows/create-prd/data/domain-complexity.csv +0 -15
  93. package/src/bmm-skills/2-plan-workflows/create-prd/data/project-types.csv +0 -11
  94. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-01-discovery.md +0 -224
  95. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-02-format-detection.md +0 -191
  96. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-02b-parity-check.md +0 -209
  97. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-03-density-validation.md +0 -174
  98. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-04-brief-coverage-validation.md +0 -214
  99. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-05-measurability-validation.md +0 -228
  100. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-06-traceability-validation.md +0 -217
  101. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-07-implementation-leakage-validation.md +0 -205
  102. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-08-domain-compliance-validation.md +0 -243
  103. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-09-project-type-validation.md +0 -263
  104. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-10-smart-validation.md +0 -209
  105. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-11-holistic-quality-validation.md +0 -264
  106. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-12-completeness-validation.md +0 -242
  107. package/src/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-13-report-complete.md +0 -232
  108. package/src/bmm-skills/2-plan-workflows/create-prd/workflow-validate-prd.md +0 -65
  109. package/src/bmm-skills/4-implementation/bmad-agent-qa/SKILL.md +0 -59
  110. package/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml +0 -11
  111. package/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/SKILL.md +0 -51
  112. package/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml +0 -11
  113. package/src/bmm-skills/4-implementation/bmad-agent-sm/SKILL.md +0 -53
  114. package/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml +0 -11
  115. package/src/core-skills/bmad-init/SKILL.md +0 -100
  116. package/src/core-skills/bmad-init/resources/core-module.yaml +0 -25
  117. package/src/core-skills/bmad-init/scripts/bmad_init.py +0 -624
  118. package/src/core-skills/bmad-init/scripts/tests/test_bmad_init.py +0 -393
  119. package/src/core-skills/bmad-party-mode/steps/step-01-agent-loading.md +0 -138
  120. package/src/core-skills/bmad-party-mode/steps/step-02-discussion-orchestration.md +0 -187
  121. package/src/core-skills/bmad-party-mode/steps/step-03-graceful-exit.md +0 -167
  122. package/src/core-skills/bmad-party-mode/workflow.md +0 -190
  123. package/tools/installer/core/custom-module-cache.js +0 -260
  124. package/tools/installer/custom-handler.js +0 -112
  125. package/tools/installer/modules/custom-modules.js +0 -197
  126. /package/src/bmm-skills/2-plan-workflows/{create-prd → bmad-edit-prd}/data/prd-purpose.md +0 -0
@@ -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
- // Separator class for visual grouping in select/multiselect prompts
10
- // Note: @clack/prompts doesn't support separators natively, they are filtered out
11
- class Separator {
12
- constructor(text = '────────') {
13
- this.line = text;
14
- this.name = text;
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');
15
21
  }
16
- type = 'separator';
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
29
+ }
30
+ return '';
17
31
  }
18
32
 
19
- // Separator for choice lists (compatible interface)
20
- const choiceUtils = { Separator };
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 (installedVersion !== 'unknown') {
101
+ if (existingInstall.installed) {
82
102
  choices.push({
83
- name: `Quick Update (v${installedVersion} → v${currentVersion})`,
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
  }
@@ -186,120 +167,6 @@ class UI {
186
167
  selectedModules = await this.selectAllModules(installedModuleIds);
187
168
  }
188
169
 
189
- // After module selection, ask about custom modules
190
- let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
191
-
192
- if (options.customContent) {
193
- // Use custom content from command-line
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
- }
295
- }
296
- }
297
-
298
- // Merge any selected custom modules
299
- if (customModuleResult.selectedCustomModules.length > 0) {
300
- selectedModules.push(...customModuleResult.selectedCustomModules);
301
- }
302
-
303
170
  // Ensure core is in the modules list
304
171
  if (!selectedModules.includes('core')) {
305
172
  selectedModules.unshift('core');
@@ -318,7 +185,6 @@ class UI {
318
185
  skipIde: toolSelection.skipIde,
319
186
  coreConfig: moduleConfigs.core || {},
320
187
  moduleConfigs: moduleConfigs,
321
- customContent: customModuleResult.customContentConfig,
322
188
  skipPrompts: options.yes || false,
323
189
  };
324
190
  }
@@ -344,84 +210,6 @@ class UI {
344
210
  selectedModules = await this.selectAllModules(installedModuleIds);
345
211
  }
346
212
 
347
- // Ask about custom content (local modules/agents/workflows)
348
- if (options.customContent) {
349
- // Use custom content from command-line
350
- const paths = options.customContent
351
- .split(',')
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();
417
- }
418
- }
419
-
420
- // Add custom content modules if any were selected
421
- if (customContentConfig && customContentConfig.selectedModuleIds) {
422
- selectedModules.push(...customContentConfig.selectedModuleIds);
423
- }
424
-
425
213
  // Ensure core is in the modules list
426
214
  if (!selectedModules.includes('core')) {
427
215
  selectedModules.unshift('core');
@@ -437,7 +225,6 @@ class UI {
437
225
  skipIde: toolSelection.skipIde,
438
226
  coreConfig: moduleConfigs.core || {},
439
227
  moduleConfigs: moduleConfigs,
440
- customContent: customContentConfig,
441
228
  skipPrompts: options.yes || false,
442
229
  };
443
230
  }
@@ -776,166 +563,80 @@ class UI {
776
563
  }
777
564
 
778
565
  /**
779
- * Get module choices for selection
566
+ * Select all modules across three tiers: official, community, and custom URL.
780
567
  * @param {Set} installedModuleIds - Currently installed module IDs
781
- * @param {Object} customContentConfig - Custom content configuration
782
- * @returns {Array} Module choices for prompt
568
+ * @returns {Array} Selected module codes (excluding core)
783
569
  */
784
- async getModuleChoices(installedModuleIds, customContentConfig = null) {
785
- const color = await prompts.getColor();
786
- const moduleChoices = [];
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
- }
570
+ async selectAllModules(installedModuleIds = new Set()) {
571
+ // Phase 1: Official modules
572
+ const officialSelected = await this._selectOfficialModules(installedModuleIds);
846
573
 
847
- // Add official modules (only non-custom ones)
848
- for (const mod of availableModules) {
849
- if (!mod.isCustom) {
850
- moduleChoices.push({
851
- name: mod.name,
852
- value: mod.id,
853
- checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
854
- hint: mod.description || undefined,
855
- });
574
+ // Determine which installed modules are NOT official (community or custom).
575
+ // These must be preserved even if the user declines to browse community/custom.
576
+ const officialCodes = new Set(officialSelected);
577
+ const externalManager = new ExternalModuleManager();
578
+ const registryModules = await externalManager.listAvailable();
579
+ const officialRegistryCodes = new Set(registryModules.map((m) => m.code));
580
+ const installedNonOfficial = [...installedModuleIds].filter((id) => !officialRegistryCodes.has(id));
581
+
582
+ // Phase 2: Community modules (category drill-down)
583
+ // Returns { codes, didBrowse } so we know if the user entered the flow
584
+ const communityResult = await this._browseCommunityModules(installedModuleIds);
585
+
586
+ // Phase 3: Custom URL modules
587
+ const customSelected = await this._addCustomUrlModules(installedModuleIds);
588
+
589
+ // Merge all selections
590
+ const allSelected = new Set([...officialSelected, ...communityResult.codes, ...customSelected]);
591
+
592
+ // Auto-include installed non-official modules that the user didn't get
593
+ // a chance to manage (they declined to browse). If they did browse,
594
+ // trust their selections - they could have deselected intentionally.
595
+ if (!communityResult.didBrowse) {
596
+ for (const code of installedNonOfficial) {
597
+ allSelected.add(code);
856
598
  }
857
599
  }
858
600
 
859
- return moduleChoices;
601
+ return [...allSelected];
860
602
  }
861
603
 
862
604
  /**
863
- * Select all modules (official + community) using grouped multiselect.
864
- * Core is shown as locked but filtered from the result since it's always installed separately.
605
+ * Select official modules using autocompleteMultiselect.
606
+ * Extracted from the original selectAllModules - unchanged behavior.
865
607
  * @param {Set} installedModuleIds - Currently installed module IDs
866
- * @returns {Array} Selected module codes (excluding core)
608
+ * @returns {Array} Selected official module codes
867
609
  */
868
- async selectAllModules(installedModuleIds = new Set()) {
869
- const { OfficialModules } = require('./modules/official-modules');
870
- const officialModulesSource = new OfficialModules();
871
- const { modules: localModules } = await officialModulesSource.listAvailable();
872
-
873
- // Get external modules
610
+ async _selectOfficialModules(installedModuleIds = new Set()) {
874
611
  const externalManager = new ExternalModuleManager();
875
- const externalModules = await externalManager.listAvailable();
612
+ const registryModules = await externalManager.listAvailable();
876
613
 
877
- // Build flat options list with group hints for autocompleteMultiselect
878
614
  const allOptions = [];
879
615
  const initialValues = [];
880
616
  const lockedValues = ['core'];
881
617
 
882
- // Core module is always installed — show it locked at the top
883
- allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' });
884
- initialValues.push('core');
885
-
886
- // Helper to build module entry with proper sorting and selection
887
- const buildModuleEntry = (mod, value, group) => {
888
- const isInstalled = installedModuleIds.has(value);
618
+ const buildModuleEntry = async (mod) => {
619
+ const isInstalled = installedModuleIds.has(mod.code);
620
+ const version = await getMarketplaceVersion(mod.code);
621
+ const label = version ? `${mod.name} (v${version})` : mod.name;
889
622
  return {
890
- label: mod.name,
891
- value,
892
- hint: mod.description || group,
893
- // Pre-select only if already installed (not on fresh install)
623
+ label,
624
+ value: mod.code,
625
+ hint: mod.description,
894
626
  selected: isInstalled,
895
627
  };
896
628
  };
897
629
 
898
- // Local modules (BMM, BMB, etc.)
899
- const localEntries = [];
900
- for (const mod of localModules) {
901
- if (!mod.isCustom && mod.id !== 'core') {
902
- const entry = buildModuleEntry(mod, mod.id, 'Local');
903
- localEntries.push(entry);
904
- if (entry.selected) {
905
- initialValues.push(mod.id);
906
- }
630
+ for (const mod of registryModules) {
631
+ const entry = await buildModuleEntry(mod);
632
+ allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
633
+ if (entry.selected) {
634
+ initialValues.push(mod.code);
907
635
  }
908
636
  }
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
- }
920
- }
921
- }
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
637
 
937
638
  const selected = await prompts.autocompleteMultiselect({
938
- message: 'Select modules to install:',
639
+ message: 'Select official modules to install:',
939
640
  options: allOptions,
940
641
  initialValues: initialValues.length > 0 ? initialValues : undefined,
941
642
  lockedValues,
@@ -945,34 +646,275 @@ class UI {
945
646
 
946
647
  const result = selected ? [...selected] : [];
947
648
 
948
- // Display selected modules as bulleted list
949
649
  if (result.length > 0) {
950
650
  const moduleLines = result.map((moduleId) => {
951
651
  const opt = allOptions.find((o) => o.value === moduleId);
952
652
  return ` \u2022 ${opt?.label || moduleId}`;
953
653
  });
954
- await prompts.log.message('Selected modules:\n' + moduleLines.join('\n'));
654
+ await prompts.log.message('Selected official modules:\n' + moduleLines.join('\n'));
955
655
  }
956
656
 
957
657
  return result;
958
658
  }
959
659
 
660
+ /**
661
+ * Browse and select community modules using category drill-down.
662
+ * Featured/promoted modules appear at the top.
663
+ * @param {Set} installedModuleIds - Currently installed module IDs
664
+ * @returns {Object} { codes: string[], didBrowse: boolean }
665
+ */
666
+ async _browseCommunityModules(installedModuleIds = new Set()) {
667
+ const browseCommunity = await prompts.confirm({
668
+ message: 'Would you like to browse community modules?',
669
+ default: false,
670
+ });
671
+ if (!browseCommunity) return { codes: [], didBrowse: false };
672
+
673
+ const { CommunityModuleManager } = require('./modules/community-manager');
674
+ const communityMgr = new CommunityModuleManager();
675
+
676
+ const s = await prompts.spinner();
677
+ s.start('Loading community module catalog...');
678
+
679
+ let categories, featured, allCommunity;
680
+ try {
681
+ [categories, featured, allCommunity] = await Promise.all([
682
+ communityMgr.getCategoryList(),
683
+ communityMgr.listFeatured(),
684
+ communityMgr.listAll(),
685
+ ]);
686
+ s.stop(`Community catalog loaded (${allCommunity.length} modules)`);
687
+ } catch (error) {
688
+ s.error('Failed to load community catalog');
689
+ await prompts.log.warn(` ${error.message}`);
690
+ return { codes: [], didBrowse: false };
691
+ }
692
+
693
+ if (allCommunity.length === 0) {
694
+ await prompts.log.info('No community modules are currently available.');
695
+ return { codes: [], didBrowse: false };
696
+ }
697
+
698
+ const selectedCodes = new Set();
699
+ let browsing = true;
700
+
701
+ while (browsing) {
702
+ const categoryChoices = [];
703
+
704
+ // Featured section at top
705
+ if (featured.length > 0) {
706
+ categoryChoices.push({
707
+ value: '__featured__',
708
+ label: `\u2605 Featured (${featured.length} module${featured.length === 1 ? '' : 's'})`,
709
+ });
710
+ }
711
+
712
+ // Categories with module counts
713
+ for (const cat of categories) {
714
+ categoryChoices.push({
715
+ value: cat.slug,
716
+ label: `${cat.name} (${cat.moduleCount} module${cat.moduleCount === 1 ? '' : 's'})`,
717
+ });
718
+ }
719
+
720
+ // Special actions at bottom
721
+ categoryChoices.push(
722
+ { value: '__all__', label: '\u25CE View all community modules' },
723
+ { value: '__search__', label: '\u25CE Search by keyword' },
724
+ { value: '__done__', label: '\u2713 Done browsing' },
725
+ );
726
+
727
+ const selectedCount = selectedCodes.size;
728
+ const categoryChoice = await prompts.select({
729
+ message: `Browse community modules${selectedCount > 0 ? ` (${selectedCount} selected)` : ''}:`,
730
+ choices: categoryChoices,
731
+ });
732
+
733
+ if (categoryChoice === '__done__') {
734
+ browsing = false;
735
+ continue;
736
+ }
737
+
738
+ let modulesToShow;
739
+ switch (categoryChoice) {
740
+ case '__featured__': {
741
+ modulesToShow = featured;
742
+
743
+ break;
744
+ }
745
+ case '__all__': {
746
+ modulesToShow = allCommunity;
747
+
748
+ break;
749
+ }
750
+ case '__search__': {
751
+ const query = await prompts.text({
752
+ message: 'Search community modules:',
753
+ placeholder: 'e.g., design, testing, game',
754
+ });
755
+ if (!query || query.trim() === '') continue;
756
+ modulesToShow = await communityMgr.searchByKeyword(query.trim());
757
+ if (modulesToShow.length === 0) {
758
+ await prompts.log.warn('No matching modules found.');
759
+ continue;
760
+ }
761
+
762
+ break;
763
+ }
764
+ default: {
765
+ modulesToShow = await communityMgr.listByCategory(categoryChoice);
766
+ }
767
+ }
768
+
769
+ // Build options for autocompleteMultiselect
770
+ const trustBadge = (tier) => {
771
+ if (tier === 'bmad-certified') return '\u2713';
772
+ if (tier === 'community-reviewed') return '\u25CB';
773
+ return '\u26A0';
774
+ };
775
+
776
+ const options = modulesToShow.map((mod) => {
777
+ const versionStr = mod.version ? ` (v${mod.version})` : '';
778
+ const badge = trustBadge(mod.trustTier);
779
+ return {
780
+ label: `${mod.displayName}${versionStr} [${badge}]`,
781
+ value: mod.code,
782
+ hint: mod.description,
783
+ };
784
+ });
785
+
786
+ // Pre-check modules that are already selected or installed
787
+ const initialValues = modulesToShow.filter((m) => selectedCodes.has(m.code) || installedModuleIds.has(m.code)).map((m) => m.code);
788
+
789
+ const selected = await prompts.autocompleteMultiselect({
790
+ message: 'Select community modules:',
791
+ options,
792
+ initialValues: initialValues.length > 0 ? initialValues : undefined,
793
+ required: false,
794
+ maxItems: Math.min(options.length, 10),
795
+ });
796
+
797
+ // Update accumulated selections: sync with what user selected in this view
798
+ const shownCodes = new Set(modulesToShow.map((m) => m.code));
799
+ for (const code of shownCodes) {
800
+ if (selected && selected.includes(code)) {
801
+ selectedCodes.add(code);
802
+ } else {
803
+ selectedCodes.delete(code);
804
+ }
805
+ }
806
+ }
807
+
808
+ if (selectedCodes.size > 0) {
809
+ const moduleLines = [];
810
+ for (const code of selectedCodes) {
811
+ const mod = await communityMgr.getModuleByCode(code);
812
+ moduleLines.push(` \u2022 ${mod?.displayName || code}`);
813
+ }
814
+ await prompts.log.message('Selected community modules:\n' + moduleLines.join('\n'));
815
+ }
816
+
817
+ return { codes: [...selectedCodes], didBrowse: true };
818
+ }
819
+
820
+ /**
821
+ * Prompt user to install modules from custom GitHub URLs.
822
+ * @param {Set} installedModuleIds - Currently installed module IDs
823
+ * @returns {Array} Selected custom module code strings
824
+ */
825
+ async _addCustomUrlModules(installedModuleIds = new Set()) {
826
+ const addCustom = await prompts.confirm({
827
+ message: 'Would you like to install from a custom GitHub URL?',
828
+ default: false,
829
+ });
830
+ if (!addCustom) return [];
831
+
832
+ const { CustomModuleManager } = require('./modules/custom-module-manager');
833
+ const customMgr = new CustomModuleManager();
834
+ const selectedModules = [];
835
+
836
+ let addMore = true;
837
+ while (addMore) {
838
+ const url = await prompts.text({
839
+ message: 'GitHub repository URL:',
840
+ placeholder: 'https://github.com/owner/repo',
841
+ validate: (input) => {
842
+ if (!input || input.trim() === '') return 'URL is required';
843
+ const result = customMgr.validateGitHubUrl(input.trim());
844
+ return result.isValid ? undefined : result.error;
845
+ },
846
+ });
847
+
848
+ const s = await prompts.spinner();
849
+ s.start('Fetching module info...');
850
+
851
+ try {
852
+ const plugins = await customMgr.discoverModules(url.trim());
853
+ s.stop('Module info loaded');
854
+
855
+ await prompts.log.warn(
856
+ 'UNVERIFIED MODULE: This module has not been reviewed by the BMad team.\n' + ' Only install modules from sources you trust.',
857
+ );
858
+
859
+ for (const plugin of plugins) {
860
+ const versionStr = plugin.version ? ` v${plugin.version}` : '';
861
+ await prompts.log.info(` ${plugin.name}${versionStr}\n ${plugin.description}\n Author: ${plugin.author}`);
862
+ }
863
+
864
+ const confirmInstall = await prompts.confirm({
865
+ message: `Install ${plugins.length} plugin${plugins.length === 1 ? '' : 's'} from ${url.trim()}?`,
866
+ default: false,
867
+ });
868
+
869
+ if (confirmInstall) {
870
+ // Pre-clone the repo so it's cached for the install pipeline
871
+ s.start('Cloning repository...');
872
+ try {
873
+ await customMgr.cloneRepo(url.trim());
874
+ s.stop('Repository cloned');
875
+ } catch (cloneError) {
876
+ s.error('Failed to clone repository');
877
+ await prompts.log.error(` ${cloneError.message}`);
878
+ addMore = await prompts.confirm({ message: 'Try another URL?', default: false });
879
+ continue;
880
+ }
881
+
882
+ for (const plugin of plugins) {
883
+ selectedModules.push(plugin.code);
884
+ }
885
+ }
886
+ } catch (error) {
887
+ s.error('Failed to load module info');
888
+ await prompts.log.error(` ${error.message}`);
889
+ }
890
+
891
+ addMore = await prompts.confirm({
892
+ message: 'Add another custom module?',
893
+ default: false,
894
+ });
895
+ }
896
+
897
+ if (selectedModules.length > 0) {
898
+ await prompts.log.message('Selected custom modules:\n' + selectedModules.map((c) => ` \u2022 ${c}`).join('\n'));
899
+ }
900
+
901
+ return selectedModules;
902
+ }
903
+
960
904
  /**
961
905
  * Get default modules for non-interactive mode
962
906
  * @param {Set} installedModuleIds - Already installed module IDs
963
907
  * @returns {Array} Default module codes
964
908
  */
965
909
  async getDefaultModules(installedModuleIds = new Set()) {
966
- const { OfficialModules } = require('./modules/official-modules');
967
- const officialModules = new OfficialModules();
968
- const { modules: localModules } = await officialModules.listAvailable();
910
+ const externalManager = new ExternalModuleManager();
911
+ const registryModules = await externalManager.listAvailable();
969
912
 
970
913
  const defaultModules = [];
971
914
 
972
- // Add default-selected local modules (typically BMM)
973
- for (const mod of localModules) {
974
- if (mod.defaultSelected === true || installedModuleIds.has(mod.id)) {
975
- defaultModules.push(mod.id);
915
+ for (const mod of registryModules) {
916
+ if (mod.defaultSelected || installedModuleIds.has(mod.code)) {
917
+ defaultModules.push(mod.code);
976
918
  }
977
919
  }
978
920
 
@@ -1273,282 +1215,6 @@ class UI {
1273
1215
  return existingInstall.ides;
1274
1216
  }
1275
1217
 
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
1218
  /**
1553
1219
  * Display module versions with update availability
1554
1220
  * @param {Array} modules - Array of module info objects with version info
@@ -1558,6 +1224,7 @@ class UI {
1558
1224
  // Group modules by source
1559
1225
  const builtIn = modules.filter((m) => m.source === 'built-in');
1560
1226
  const external = modules.filter((m) => m.source === 'external');
1227
+ const community = modules.filter((m) => m.source === 'community');
1561
1228
  const custom = modules.filter((m) => m.source === 'custom');
1562
1229
  const unknown = modules.filter((m) => m.source === 'unknown');
1563
1230
 
@@ -1578,6 +1245,7 @@ class UI {
1578
1245
 
1579
1246
  formatGroup(builtIn, 'Built-in Modules');
1580
1247
  formatGroup(external, 'External Modules (Official)');
1248
+ formatGroup(community, 'Community Modules');
1581
1249
  formatGroup(custom, 'Custom Modules');
1582
1250
  formatGroup(unknown, 'Other Modules');
1583
1251