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.
Files changed (72) hide show
  1. package/.claude-plugin/marketplace.json +0 -3
  2. package/README.md +8 -9
  3. package/README_CN.md +1 -1
  4. package/README_VN.md +110 -0
  5. package/package.json +1 -1
  6. package/removals.txt +17 -0
  7. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-13-responsive-accessibility.md +1 -1
  8. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/data/prd-purpose.md +197 -0
  9. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01-discovery.md +1 -1
  10. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01b-legacy-conversion.md +1 -1
  11. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-02-review.md +1 -1
  12. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-03-edit.md +1 -1
  13. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +1 -3
  14. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-01-document-discovery.md +1 -1
  15. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-02-prd-analysis.md +1 -1
  16. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-03-epic-coverage-validation.md +1 -1
  17. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +1 -1
  18. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +1 -1
  19. package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +5 -0
  20. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/SKILL.md +29 -0
  21. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/generate-trail.md +38 -0
  22. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-01-orientation.md +105 -0
  23. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-02-walkthrough.md +89 -0
  24. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-03-detail-pass.md +106 -0
  25. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-04-testing.md +74 -0
  26. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-05-wrapup.md +24 -0
  27. package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-01-gather-context.md +38 -15
  28. package/src/bmm-skills/4-implementation/bmad-correct-course/checklist.md +2 -2
  29. package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +8 -8
  30. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/checklist.md +1 -1
  31. package/src/bmm-skills/4-implementation/bmad-quick-dev/compile-epic-context.md +62 -0
  32. package/src/bmm-skills/4-implementation/bmad-quick-dev/spec-template.md +1 -1
  33. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md +33 -6
  34. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-02-plan.md +20 -8
  35. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-03-implement.md +2 -0
  36. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +16 -4
  37. package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +1 -5
  38. package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +134 -134
  39. package/src/bmm-skills/4-implementation/bmad-sprint-planning/sprint-status-template.yaml +1 -1
  40. package/src/bmm-skills/4-implementation/bmad-sprint-planning/workflow.md +3 -3
  41. package/src/bmm-skills/4-implementation/bmad-sprint-status/workflow.md +2 -2
  42. package/src/bmm-skills/module-help.csv +2 -0
  43. package/src/core-skills/bmad-help/SKILL.md +4 -2
  44. package/src/core-skills/bmad-party-mode/SKILL.md +8 -6
  45. package/src/core-skills/module-help.csv +1 -0
  46. package/tools/installer/cli-utils.js +18 -9
  47. package/tools/installer/commands/install.js +1 -1
  48. package/tools/installer/core/existing-install.js +2 -8
  49. package/tools/installer/core/install-paths.js +0 -3
  50. package/tools/installer/core/installer.js +180 -463
  51. package/tools/installer/core/manifest-generator.js +8 -14
  52. package/tools/installer/core/manifest.js +94 -102
  53. package/tools/installer/ide/_config-driven.js +149 -38
  54. package/tools/installer/ide/shared/skill-manifest.js +1 -16
  55. package/tools/installer/install-messages.yaml +19 -26
  56. package/tools/installer/modules/community-manager.js +377 -0
  57. package/tools/installer/modules/custom-module-manager.js +644 -0
  58. package/tools/installer/modules/external-manager.js +65 -49
  59. package/tools/installer/modules/official-modules.js +117 -65
  60. package/tools/installer/modules/plugin-resolver.js +398 -0
  61. package/tools/installer/modules/registry-client.js +66 -0
  62. package/tools/installer/{external-official-modules.yaml → modules/registry-fallback.yaml} +3 -12
  63. package/tools/installer/ui.js +549 -666
  64. package/src/bmm-skills/4-implementation/bmad-agent-qa/SKILL.md +0 -61
  65. package/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml +0 -11
  66. package/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/SKILL.md +0 -53
  67. package/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml +0 -11
  68. package/src/bmm-skills/4-implementation/bmad-agent-sm/SKILL.md +0 -55
  69. package/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml +0 -11
  70. package/tools/installer/core/custom-module-cache.js +0 -260
  71. package/tools/installer/custom-handler.js +0 -112
  72. package/tools/installer/modules/custom-modules.js +0 -197
@@ -2,7 +2,6 @@ const path = require('node:path');
2
2
  const fs = require('fs-extra');
3
3
  const { Manifest } = require('./manifest');
4
4
  const { OfficialModules } = require('../modules/official-modules');
5
- const { CustomModules } = require('../modules/custom-modules');
6
5
  const { IdeManager } = require('../ide/manager');
7
6
  const { FileOps } = require('../file-ops');
8
7
  const { Config } = require('./config');
@@ -19,13 +18,50 @@ class Installer {
19
18
  constructor() {
20
19
  this.externalModuleManager = new ExternalModuleManager();
21
20
  this.manifest = new Manifest();
22
- this.customModules = new CustomModules();
23
21
  this.ideManager = new IdeManager();
24
22
  this.fileOps = new FileOps();
25
23
  this.installedFiles = new Set(); // Track all installed files
26
24
  this.bmadFolderName = BMAD_FOLDER_NAME;
27
25
  }
28
26
 
27
+ /**
28
+ * Read the module version from .claude-plugin/marketplace.json
29
+ * Walks up from sourcePath looking for .claude-plugin/marketplace.json
30
+ * @param {string} sourcePath - Module source directory
31
+ * @returns {string} Version string or empty string
32
+ */
33
+ async _getMarketplaceVersion(sourcePath) {
34
+ let dir = sourcePath;
35
+ for (let i = 0; i < 5; i++) {
36
+ const marketplacePath = path.join(dir, '.claude-plugin', 'marketplace.json');
37
+ if (await fs.pathExists(marketplacePath)) {
38
+ try {
39
+ const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
40
+ return this._extractMarketplaceVersion(data);
41
+ } catch {
42
+ return '';
43
+ }
44
+ }
45
+ const parent = path.dirname(dir);
46
+ if (parent === dir) break;
47
+ dir = parent;
48
+ }
49
+ return '';
50
+ }
51
+
52
+ /**
53
+ * Extract the highest version from marketplace.json plugins array
54
+ */
55
+ _extractMarketplaceVersion(data) {
56
+ const plugins = data?.plugins;
57
+ if (!Array.isArray(plugins) || plugins.length === 0) return '';
58
+ let best = '';
59
+ for (const p of plugins) {
60
+ if (p.version && (!best || p.version > best)) best = p.version;
61
+ }
62
+ return best;
63
+ }
64
+
29
65
  /**
30
66
  * Main installation method
31
67
  * @param {Object} config - Installation configuration
@@ -42,8 +78,6 @@ class Installer {
42
78
  const officialModules = await OfficialModules.build(config, paths);
43
79
  const existingInstall = await ExistingInstall.detect(paths.bmadDir);
44
80
 
45
- await this.customModules.discoverPaths(originalConfig, paths);
46
-
47
81
  if (existingInstall.installed) {
48
82
  await this._removeDeselectedModules(existingInstall, config, paths);
49
83
  updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
@@ -52,20 +86,46 @@ class Installer {
52
86
 
53
87
  await this._validateIdeSelection(config);
54
88
 
89
+ // Capture pre-install module versions for from→to display
90
+ const preInstallVersions = new Map();
91
+ if (existingInstall.installed) {
92
+ const existingModules = await this.manifest.getAllModuleVersions(paths.bmadDir);
93
+ for (const mod of existingModules) {
94
+ if (mod.name && mod.version) {
95
+ preInstallVersions.set(mod.name, mod.version);
96
+ }
97
+ }
98
+ }
99
+
55
100
  // Results collector for consolidated summary
56
101
  const results = [];
57
- const addResult = (step, status, detail = '') => results.push({ step, status, detail });
102
+ const addResult = (step, status, detail = '', meta = {}) => results.push({ step, status, detail, ...meta });
103
+
104
+ // Capture previously installed skill IDs before they get overwritten
105
+ const previousSkillIds = new Set();
106
+ const prevCsvPath = path.join(paths.bmadDir, '_config', 'skill-manifest.csv');
107
+ if (await fs.pathExists(prevCsvPath)) {
108
+ try {
109
+ const csvParse = require('csv-parse/sync');
110
+ const content = await fs.readFile(prevCsvPath, 'utf8');
111
+ const records = csvParse.parse(content, { columns: true, skip_empty_lines: true });
112
+ for (const r of records) {
113
+ if (r.canonicalId) previousSkillIds.add(r.canonicalId);
114
+ }
115
+ } catch (error) {
116
+ await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
117
+ }
118
+ }
58
119
 
59
- await this._cacheCustomModules(paths, addResult);
120
+ const allModules = config.modules || [];
60
121
 
61
- // Compute module lists: official = selected minus custom, all = both
62
- const customModuleIds = new Set(this.customModules.paths.keys());
63
- const officialModuleIds = (config.modules || []).filter((m) => !customModuleIds.has(m));
64
- const allModules = [...officialModuleIds, ...[...customModuleIds].filter((id) => !officialModuleIds.includes(id))];
122
+ await this._installAndConfigure(config, originalConfig, paths, allModules, allModules, addResult, officialModules);
65
123
 
66
- await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules);
124
+ await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
67
125
 
68
- await this._setupIdes(config, allModules, paths, addResult);
126
+ // Skills are now in IDE directories — remove redundant copies from _bmad/.
127
+ // Also cleans up skill dirs left by older installer versions.
128
+ await this._cleanupSkillDirs(paths.bmadDir);
69
129
 
70
130
  const restoreResult = await this._restoreUserFiles(paths, updateState);
71
131
 
@@ -76,6 +136,7 @@ class Installer {
76
136
  ides: config.ides,
77
137
  customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
78
138
  modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
139
+ preInstallVersions,
79
140
  });
80
141
 
81
142
  return {
@@ -172,26 +233,6 @@ class Installer {
172
233
  }
173
234
  }
174
235
 
175
- /**
176
- * Cache custom modules into the local cache directory.
177
- * Updates this.customModules.paths in place with cached locations.
178
- */
179
- async _cacheCustomModules(paths, addResult) {
180
- if (!this.customModules.paths || this.customModules.paths.size === 0) return;
181
-
182
- const { CustomModuleCache } = require('./custom-module-cache');
183
- const customCache = new CustomModuleCache(paths.bmadDir);
184
-
185
- for (const [moduleId, sourcePath] of this.customModules.paths) {
186
- const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
187
- sourcePath: sourcePath,
188
- });
189
- this.customModules.paths.set(moduleId, cachedInfo.cachePath);
190
- }
191
-
192
- addResult('Custom modules cached', 'ok');
193
- }
194
-
195
236
  /**
196
237
  * Install modules, create directories, generate configs and manifests.
197
238
  */
@@ -214,11 +255,6 @@ class Installer {
214
255
  installedModuleNames,
215
256
  });
216
257
 
217
- await this._installCustomModules(config, paths, addResult, officialModules, {
218
- message,
219
- installedModuleNames,
220
- });
221
-
222
258
  return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
223
259
  },
224
260
  });
@@ -321,7 +357,7 @@ class Installer {
321
357
  /**
322
358
  * Set up IDE integrations for each selected IDE.
323
359
  */
324
- async _setupIdes(config, allModules, paths, addResult) {
360
+ async _setupIdes(config, allModules, paths, addResult, previousSkillIds = new Set()) {
325
361
  if (config.skipIde || !config.ides || config.ides.length === 0) return;
326
362
 
327
363
  await this.ideManager.ensureInitialized();
@@ -336,6 +372,7 @@ class Installer {
336
372
  const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
337
373
  selectedModules: allModules || [],
338
374
  verbose: config.verbose,
375
+ previousSkillIds,
339
376
  });
340
377
 
341
378
  if (setupResult.success) {
@@ -346,6 +383,33 @@ class Installer {
346
383
  }
347
384
  }
348
385
 
386
+ /**
387
+ * Remove skill directories from _bmad/ after IDE installation.
388
+ * Skills are self-contained in IDE directories, so _bmad/ only needs
389
+ * module-level files (config.yaml, _config/, etc.).
390
+ * Also cleans up skill dirs left by older installer versions.
391
+ * @param {string} bmadDir - BMAD installation directory
392
+ */
393
+ async _cleanupSkillDirs(bmadDir) {
394
+ const csv = require('csv-parse/sync');
395
+ const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
396
+ if (!(await fs.pathExists(csvPath))) return;
397
+
398
+ const csvContent = await fs.readFile(csvPath, 'utf8');
399
+ const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true });
400
+ const bmadFolderName = path.basename(bmadDir);
401
+ const bmadPrefix = bmadFolderName + '/';
402
+
403
+ for (const record of records) {
404
+ if (!record.path) continue;
405
+ const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;
406
+ const sourceDir = path.dirname(path.join(bmadDir, relativePath));
407
+ if (await fs.pathExists(sourceDir)) {
408
+ await fs.remove(sourceDir);
409
+ }
410
+ }
411
+ }
412
+
349
413
  /**
350
414
  * Restore custom and modified files that were backed up before the update.
351
415
  * No-op for fresh installs (updateState is null).
@@ -417,48 +481,7 @@ class Installer {
417
481
  }
418
482
 
419
483
  /**
420
- * Scan the custom module cache directory and register any cached custom modules
421
- * that aren't already known from the manifest or external module list.
422
- * @param {Object} paths - InstallPaths instance
423
- */
424
- async _scanCachedCustomModules(paths) {
425
- const cacheDir = paths.customCacheDir;
426
- if (!(await fs.pathExists(cacheDir))) {
427
- return;
428
- }
429
-
430
- const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
431
-
432
- for (const cachedModule of cachedModules) {
433
- const moduleId = cachedModule.name;
434
- const cachedPath = path.join(cacheDir, moduleId);
435
-
436
- // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
437
- if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
438
- continue;
439
- }
440
-
441
- // Skip if we already have this module from manifest
442
- if (this.customModules.paths.has(moduleId)) {
443
- continue;
444
- }
445
-
446
- // Check if this is an external official module - skip cache for those
447
- const isExternal = await this.externalModuleManager.hasModule(moduleId);
448
- if (isExternal) {
449
- continue;
450
- }
451
-
452
- // Check if this is actually a custom module (has module.yaml)
453
- const moduleYamlPath = path.join(cachedPath, 'module.yaml');
454
- if (await fs.pathExists(moduleYamlPath)) {
455
- this.customModules.paths.set(moduleId, cachedPath);
456
- }
457
- }
458
- }
459
-
460
- /**
461
- * Common update preparation: detect files, preserve core config, scan cache, back up.
484
+ * Common update preparation: detect files, preserve core config, back up.
462
485
  * @param {Object} paths - InstallPaths instance
463
486
  * @param {Object} config - Clean config (may have coreConfig updated)
464
487
  * @param {Object} existingInstall - Detection result
@@ -486,8 +509,6 @@ class Installer {
486
509
  }
487
510
  }
488
511
 
489
- await this._scanCachedCustomModules(paths);
490
-
491
512
  const backupDirs = await this._backupUserFiles(paths, customFiles, modifiedFiles);
492
513
 
493
514
  return {
@@ -548,6 +569,7 @@ class Installer {
548
569
  */
549
570
  async _installOfficialModules(config, paths, officialModuleIds, addResult, isQuickUpdate, officialModules, ctx) {
550
571
  const { message, installedModuleNames } = ctx;
572
+ const { CustomModuleManager } = require('../modules/custom-module-manager');
551
573
 
552
574
  for (const moduleName of officialModuleIds) {
553
575
  if (installedModuleNames.has(moduleName)) continue;
@@ -556,7 +578,7 @@ class Installer {
556
578
  message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
557
579
 
558
580
  const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
559
- await officialModules.install(
581
+ const installResult = await officialModules.install(
560
582
  moduleName,
561
583
  paths.bmadDir,
562
584
  (filePath) => {
@@ -570,35 +592,16 @@ class Installer {
570
592
  },
571
593
  );
572
594
 
573
- addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
574
- }
575
- }
576
-
577
- /**
578
- * Install custom modules using CustomModules.install().
579
- * Source paths come from this.customModules.paths (populated by discoverPaths).
580
- */
581
- async _installCustomModules(config, paths, addResult, officialModules, ctx) {
582
- const { message, installedModuleNames } = ctx;
583
- const isQuickUpdate = config.isQuickUpdate();
584
-
585
- for (const [moduleName, sourcePath] of this.customModules.paths) {
586
- if (installedModuleNames.has(moduleName)) continue;
587
- installedModuleNames.add(moduleName);
595
+ // Get display name from source module.yaml; version from resolution cache or marketplace.json
596
+ const sourcePath = await officialModules.findModuleSource(moduleName, { silent: true });
597
+ const moduleInfo = sourcePath ? await officialModules.getModuleInfo(sourcePath, moduleName, '') : null;
598
+ const displayName = moduleInfo?.name || moduleName;
588
599
 
589
- message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
590
-
591
- const collectedModuleConfig = officialModules.moduleConfigs[moduleName] || {};
592
- const result = await this.customModules.install(moduleName, paths.bmadDir, (filePath) => this.installedFiles.add(filePath), {
593
- moduleConfig: collectedModuleConfig,
594
- });
595
-
596
- // Generate runtime config.yaml with merged values
597
- await this.generateModuleConfigs(paths.bmadDir, {
598
- [moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
599
- });
600
-
601
- addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
600
+ // Prefer version from resolution cache (accurate for custom/local modules),
601
+ // fall back to marketplace.json walk-up for official modules
602
+ const cachedResolution = CustomModuleManager._resolutionCache.get(moduleName);
603
+ const version = cachedResolution?.version || (sourcePath ? await this._getMarketplaceVersion(sourcePath) : '');
604
+ addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
602
605
  }
603
606
  }
604
607
 
@@ -971,6 +974,14 @@ class Installer {
971
974
  outputs,
972
975
  ] = columns;
973
976
 
977
+ // Pass through _meta rows as-is (module metadata, not a skill)
978
+ if (phase === '_meta') {
979
+ const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
980
+ const metaRow = [finalModule, '_meta', '', '', '', '', '', 'false', '', '', '', '', '', '', outputLocation || '', ''];
981
+ allRows.push(metaRow.map((c) => this.escapeCSVField(c)).join(','));
982
+ continue;
983
+ }
984
+
974
985
  // If module column is empty, set it to this module's name (except for core which stays empty for universal tools)
975
986
  const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
976
987
 
@@ -1062,23 +1073,10 @@ class Installer {
1062
1073
  const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
1063
1074
 
1064
1075
  // Build step lines with status indicators
1076
+ const preVersions = context.preInstallVersions || new Map();
1065
1077
  const lines = [];
1066
1078
  for (const r of results) {
1067
- let stepLabel = null;
1068
-
1069
- if (r.status !== 'ok') {
1070
- stepLabel = r.step;
1071
- } else if (r.step === 'Core') {
1072
- stepLabel = 'BMAD';
1073
- } else if (r.step.startsWith('Module: ')) {
1074
- stepLabel = r.step;
1075
- } else if (selectedIdes.has(String(r.step).toLowerCase())) {
1076
- stepLabel = r.step;
1077
- }
1078
-
1079
- if (!stepLabel) {
1080
- continue;
1081
- }
1079
+ const stepLabel = r.step;
1082
1080
 
1083
1081
  let icon;
1084
1082
  if (r.status === 'ok') {
@@ -1088,18 +1086,32 @@ class Installer {
1088
1086
  } else {
1089
1087
  icon = color.red('\u2717');
1090
1088
  }
1091
- const detail = r.detail ? color.dim(` (${r.detail})`) : '';
1089
+
1090
+ // Build version detail for module results
1091
+ let detail = '';
1092
+ if (r.moduleCode && r.newVersion) {
1093
+ const oldVersion = preVersions.get(r.moduleCode);
1094
+ if (oldVersion && oldVersion === r.newVersion) {
1095
+ detail = ` (v${r.newVersion}, no change)`;
1096
+ } else if (oldVersion) {
1097
+ detail = ` (v${oldVersion} → v${r.newVersion})`;
1098
+ } else {
1099
+ detail = ` (v${r.newVersion}, installed)`;
1100
+ }
1101
+ } else if (r.detail) {
1102
+ detail = ` (${r.detail})`;
1103
+ }
1092
1104
  lines.push(` ${icon} ${stepLabel}${detail}`);
1093
1105
  }
1094
1106
 
1095
1107
  if ((context.ides || []).length === 0) {
1096
- lines.push(` ${color.green('\u2713')} No IDE selected ${color.dim('(installed in _bmad only)')}`);
1108
+ lines.push(` ${color.green('\u2713')} No IDE selected (installed in _bmad only)`);
1097
1109
  }
1098
1110
 
1099
1111
  // Context and warnings
1100
1112
  lines.push('');
1101
1113
  if (context.bmadDir) {
1102
- lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
1114
+ lines.push(` Installed to: ${context.bmadDir}`);
1103
1115
  }
1104
1116
  if (context.customFiles && context.customFiles.length > 0) {
1105
1117
  lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
@@ -1111,17 +1123,18 @@ class Installer {
1111
1123
  // Next steps
1112
1124
  lines.push(
1113
1125
  '',
1114
- ' Next steps:',
1115
- ` Read our new Docs Site: ${color.dim('https://docs.bmad-method.org/')}`,
1116
- ` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
1117
- ` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`,
1118
- ` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`,
1126
+ ' Get started:',
1127
+ ` 1. Launch your AI agent from your project folder`,
1128
+ ` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
1129
+ '',
1130
+ ` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
1131
+ ` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
1119
1132
  );
1120
- if (context.ides && context.ides.length > 0) {
1121
- lines.push(` Invoke the ${color.cyan('bmad-help')} skill in your IDE Agent to get started`);
1122
- }
1123
1133
 
1124
- await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
1134
+ await prompts.box(lines.join('\n'), 'BMAD is ready to use!', {
1135
+ rounded: true,
1136
+ formatBorder: color.green,
1137
+ });
1125
1138
  }
1126
1139
 
1127
1140
  /**
@@ -1144,63 +1157,9 @@ class Installer {
1144
1157
  const configuredIdes = existingInstall.ides;
1145
1158
  const projectRoot = path.dirname(bmadDir);
1146
1159
 
1147
- // Get custom module sources: first from --custom-content (re-cache from source), then from cache
1148
- const customModuleSources = new Map();
1149
- if (config.customContent?.sources?.length > 0) {
1150
- for (const source of config.customContent.sources) {
1151
- if (source.id && source.path && (await fs.pathExists(source.path))) {
1152
- customModuleSources.set(source.id, {
1153
- id: source.id,
1154
- name: source.name || source.id,
1155
- sourcePath: source.path,
1156
- cached: false, // From CLI, will be re-cached
1157
- });
1158
- }
1159
- }
1160
- }
1161
- const cacheDir = path.join(bmadDir, '_config', 'custom');
1162
- if (await fs.pathExists(cacheDir)) {
1163
- const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
1164
-
1165
- for (const cachedModule of cachedModules) {
1166
- const moduleId = cachedModule.name;
1167
- const cachedPath = path.join(cacheDir, moduleId);
1168
-
1169
- // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
1170
- if (!(await fs.pathExists(cachedPath))) {
1171
- continue;
1172
- }
1173
- if (!cachedModule.isDirectory()) {
1174
- continue;
1175
- }
1176
-
1177
- // Skip if we already have this module from manifest
1178
- if (customModuleSources.has(moduleId)) {
1179
- continue;
1180
- }
1181
-
1182
- // Check if this is an external official module - skip cache for those
1183
- const isExternal = await this.externalModuleManager.hasModule(moduleId);
1184
- if (isExternal) {
1185
- continue;
1186
- }
1187
-
1188
- // Check if this is actually a custom module (has module.yaml)
1189
- const moduleYamlPath = path.join(cachedPath, 'module.yaml');
1190
- if (await fs.pathExists(moduleYamlPath)) {
1191
- customModuleSources.set(moduleId, {
1192
- id: moduleId,
1193
- name: moduleId,
1194
- sourcePath: cachedPath,
1195
- cached: true,
1196
- });
1197
- }
1198
- }
1199
- }
1200
-
1201
1160
  // Get available modules (what we have source for)
1202
1161
  const availableModulesData = await new OfficialModules().listAvailable();
1203
- const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
1162
+ const availableModules = [...availableModulesData.modules];
1204
1163
 
1205
1164
  // Add external official modules to available modules
1206
1165
  const externalModules = await this.externalModuleManager.listAvailable();
@@ -1215,52 +1174,44 @@ class Installer {
1215
1174
  }
1216
1175
  }
1217
1176
 
1218
- // Add custom modules from manifest if their sources exist
1219
- for (const [moduleId, customModule] of customModuleSources) {
1220
- const sourcePath = customModule.sourcePath;
1221
- if (sourcePath && (await fs.pathExists(sourcePath)) && !availableModules.some((m) => m.id === moduleId)) {
1177
+ // Add installed community modules to available modules
1178
+ const { CommunityModuleManager } = require('../modules/community-manager');
1179
+ const communityMgr = new CommunityModuleManager();
1180
+ const communityModules = await communityMgr.listAll();
1181
+ for (const communityModule of communityModules) {
1182
+ if (installedModules.includes(communityModule.code) && !availableModules.some((m) => m.id === communityModule.code)) {
1222
1183
  availableModules.push({
1223
- id: moduleId,
1224
- name: customModule.name || moduleId,
1225
- path: sourcePath,
1226
- isCustom: true,
1227
- fromManifest: true,
1184
+ id: communityModule.code,
1185
+ name: communityModule.displayName,
1186
+ isExternal: true,
1187
+ fromCommunity: true,
1228
1188
  });
1229
1189
  }
1230
1190
  }
1231
1191
 
1232
- // Handle missing custom module sources
1233
- const customModuleResult = await this.handleMissingCustomSources(
1234
- customModuleSources,
1235
- bmadDir,
1236
- projectRoot,
1237
- 'update',
1238
- installedModules,
1239
- config.skipPrompts || false,
1240
- );
1241
-
1242
- const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
1243
-
1244
- const customModulesFromManifest = validCustomModules.map((m) => ({
1245
- ...m,
1246
- isCustom: true,
1247
- hasUpdate: true,
1248
- }));
1192
+ // Add installed custom modules to available modules
1193
+ const { CustomModuleManager } = require('../modules/custom-module-manager');
1194
+ const customMgr = new CustomModuleManager();
1195
+ for (const moduleId of installedModules) {
1196
+ if (!availableModules.some((m) => m.id === moduleId)) {
1197
+ const customSource = await customMgr.findModuleSourceByCode(moduleId, { bmadDir });
1198
+ if (customSource) {
1199
+ availableModules.push({
1200
+ id: moduleId,
1201
+ name: moduleId,
1202
+ isExternal: true,
1203
+ fromCustom: true,
1204
+ });
1205
+ }
1206
+ }
1207
+ }
1249
1208
 
1250
- const allAvailableModules = [...availableModules, ...customModulesFromManifest];
1251
- const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
1209
+ const availableModuleIds = new Set(availableModules.map((m) => m.id));
1252
1210
 
1253
1211
  // Only update modules that are BOTH installed AND available (we have source for)
1254
1212
  const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
1255
1213
  const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id));
1256
1214
 
1257
- // Add custom modules that were kept without sources to the skipped modules
1258
- for (const keptModule of keptModulesWithoutSources) {
1259
- if (!skippedModules.includes(keptModule)) {
1260
- skippedModules.push(keptModule);
1261
- }
1262
- }
1263
-
1264
1215
  if (skippedModules.length > 0) {
1265
1216
  await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`);
1266
1217
  }
@@ -1278,6 +1229,7 @@ class Installer {
1278
1229
  }
1279
1230
 
1280
1231
  for (const moduleName of modulesToUpdate) {
1232
+ if (moduleName === 'core') continue; // Already collected above
1281
1233
  const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
1282
1234
  if (modulePrompted) {
1283
1235
  promptedForNewFields = true;
@@ -1304,9 +1256,7 @@ class Installer {
1304
1256
  actionType: 'install',
1305
1257
  _quickUpdate: true,
1306
1258
  _preserveModules: skippedModules,
1307
- _customModuleSources: customModuleSources,
1308
1259
  _existingModules: installedModules,
1309
- customContent: config.customContent,
1310
1260
  };
1311
1261
 
1312
1262
  await this.install(installConfig);
@@ -1441,239 +1391,6 @@ class Installer {
1441
1391
  return this._readOutputFolder(bmadDir);
1442
1392
  }
1443
1393
 
1444
- /**
1445
- * Handle missing custom module sources interactively
1446
- * @param {Map} customModuleSources - Map of custom module ID to info
1447
- * @param {string} bmadDir - BMAD directory
1448
- * @param {string} projectRoot - Project root directory
1449
- * @param {string} operation - Current operation ('update', 'compile', etc.)
1450
- * @param {Array} installedModules - Array of installed module IDs (will be modified)
1451
- * @param {boolean} [skipPrompts=false] - Skip interactive prompts and keep all modules with missing sources
1452
- * @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
1453
- */
1454
- async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules, skipPrompts = false) {
1455
- const validCustomModules = [];
1456
- const keptModulesWithoutSources = []; // Track modules kept without sources
1457
- const customModulesWithMissingSources = [];
1458
-
1459
- // Check which sources exist
1460
- for (const [moduleId, customInfo] of customModuleSources) {
1461
- if (await fs.pathExists(customInfo.sourcePath)) {
1462
- validCustomModules.push({
1463
- id: moduleId,
1464
- name: customInfo.name,
1465
- path: customInfo.sourcePath,
1466
- info: customInfo,
1467
- });
1468
- } else {
1469
- // For cached modules that are missing, we just skip them without prompting
1470
- if (customInfo.cached) {
1471
- // Skip cached modules without prompting
1472
- keptModulesWithoutSources.push({
1473
- id: moduleId,
1474
- name: customInfo.name,
1475
- cached: true,
1476
- });
1477
- } else {
1478
- customModulesWithMissingSources.push({
1479
- id: moduleId,
1480
- name: customInfo.name,
1481
- sourcePath: customInfo.sourcePath,
1482
- relativePath: customInfo.relativePath,
1483
- info: customInfo,
1484
- });
1485
- }
1486
- }
1487
- }
1488
-
1489
- // If no missing sources, return immediately
1490
- if (customModulesWithMissingSources.length === 0) {
1491
- return {
1492
- validCustomModules,
1493
- keptModulesWithoutSources: [],
1494
- };
1495
- }
1496
-
1497
- // Non-interactive mode: keep all modules with missing sources
1498
- if (skipPrompts) {
1499
- for (const missing of customModulesWithMissingSources) {
1500
- keptModulesWithoutSources.push(missing.id);
1501
- }
1502
- return { validCustomModules, keptModulesWithoutSources };
1503
- }
1504
-
1505
- await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`);
1506
-
1507
- let keptCount = 0;
1508
- let updatedCount = 0;
1509
- let removedCount = 0;
1510
-
1511
- for (const missing of customModulesWithMissingSources) {
1512
- await prompts.log.message(
1513
- `${missing.name} (${missing.id})\n Original source: ${missing.relativePath}\n Full path: ${missing.sourcePath}`,
1514
- );
1515
-
1516
- const choices = [
1517
- {
1518
- name: 'Keep installed (will not be processed)',
1519
- value: 'keep',
1520
- hint: 'Keep',
1521
- },
1522
- {
1523
- name: 'Specify new source location',
1524
- value: 'update',
1525
- hint: 'Update',
1526
- },
1527
- ];
1528
-
1529
- // Only add remove option if not just compiling agents
1530
- if (operation !== 'compile-agents') {
1531
- choices.push({
1532
- name: '⚠️ REMOVE module completely (destructive!)',
1533
- value: 'remove',
1534
- hint: 'Remove',
1535
- });
1536
- }
1537
-
1538
- const action = await prompts.select({
1539
- message: `How would you like to handle "${missing.name}"?`,
1540
- choices,
1541
- });
1542
-
1543
- switch (action) {
1544
- case 'update': {
1545
- // Use sync validation because @clack/prompts doesn't support async validate
1546
- const newSourcePath = await prompts.text({
1547
- message: 'Enter the new path to the custom module:',
1548
- default: missing.sourcePath,
1549
- validate: (input) => {
1550
- if (!input || input.trim() === '') {
1551
- return 'Please enter a path';
1552
- }
1553
- const expandedPath = path.resolve(input.trim());
1554
- if (!fs.pathExistsSync(expandedPath)) {
1555
- return 'Path does not exist';
1556
- }
1557
- // Check if it looks like a valid module
1558
- const moduleYamlPath = path.join(expandedPath, 'module.yaml');
1559
- const agentsPath = path.join(expandedPath, 'agents');
1560
- const workflowsPath = path.join(expandedPath, 'workflows');
1561
-
1562
- if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
1563
- return 'Path does not appear to contain a valid custom module';
1564
- }
1565
- return; // clack expects undefined for valid input
1566
- },
1567
- });
1568
-
1569
- // Defensive: handleCancel should have exited, but guard against symbol propagation
1570
- if (typeof newSourcePath !== 'string') {
1571
- keptCount++;
1572
- keptModulesWithoutSources.push(missing.id);
1573
- continue;
1574
- }
1575
-
1576
- // Update the source in manifest
1577
- const resolvedPath = path.resolve(newSourcePath.trim());
1578
- missing.info.sourcePath = resolvedPath;
1579
- // Remove relativePath - we only store absolute sourcePath now
1580
- delete missing.info.relativePath;
1581
- await this.manifest.addCustomModule(bmadDir, missing.info);
1582
-
1583
- validCustomModules.push({
1584
- id: missing.id,
1585
- name: missing.name,
1586
- path: resolvedPath,
1587
- info: missing.info,
1588
- });
1589
-
1590
- updatedCount++;
1591
- await prompts.log.success('Updated source location');
1592
-
1593
- break;
1594
- }
1595
- case 'remove': {
1596
- // Extra confirmation for destructive remove
1597
- await prompts.log.error(
1598
- `WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!\n Module location: ${path.join(bmadDir, missing.id)}`,
1599
- );
1600
-
1601
- const confirmDelete = await prompts.confirm({
1602
- message: 'Are you absolutely sure you want to delete this module?',
1603
- default: false,
1604
- });
1605
-
1606
- if (confirmDelete) {
1607
- const typedConfirm = await prompts.text({
1608
- message: 'Type "DELETE" to confirm permanent deletion:',
1609
- validate: (input) => {
1610
- if (input !== 'DELETE') {
1611
- return 'You must type "DELETE" exactly to proceed';
1612
- }
1613
- return; // clack expects undefined for valid input
1614
- },
1615
- });
1616
-
1617
- if (typedConfirm === 'DELETE') {
1618
- // Remove the module from filesystem and manifest
1619
- const modulePath = path.join(bmadDir, missing.id);
1620
- if (await fs.pathExists(modulePath)) {
1621
- const fsExtra = require('fs-extra');
1622
- await fsExtra.remove(modulePath);
1623
- await prompts.log.warn(`Deleted module directory: ${path.relative(projectRoot, modulePath)}`);
1624
- }
1625
-
1626
- await this.manifest.removeModule(bmadDir, missing.id);
1627
- await this.manifest.removeCustomModule(bmadDir, missing.id);
1628
- await prompts.log.warn('Removed from manifest');
1629
-
1630
- // Also remove from installedModules list
1631
- if (installedModules && installedModules.includes(missing.id)) {
1632
- const index = installedModules.indexOf(missing.id);
1633
- if (index !== -1) {
1634
- installedModules.splice(index, 1);
1635
- }
1636
- }
1637
-
1638
- removedCount++;
1639
- await prompts.log.error(`"${missing.name}" has been permanently removed`);
1640
- } else {
1641
- await prompts.log.message('Removal cancelled - module will be kept');
1642
- keptCount++;
1643
- }
1644
- } else {
1645
- await prompts.log.message('Removal cancelled - module will be kept');
1646
- keptCount++;
1647
- }
1648
-
1649
- break;
1650
- }
1651
- case 'keep': {
1652
- keptCount++;
1653
- keptModulesWithoutSources.push(missing.id);
1654
- await prompts.log.message('Module will be kept as-is');
1655
-
1656
- break;
1657
- }
1658
- // No default
1659
- }
1660
- }
1661
-
1662
- // Show summary
1663
- if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
1664
- let summary = 'Summary for custom modules with missing sources:';
1665
- if (keptCount > 0) summary += `\n • ${keptCount} module(s) kept as-is`;
1666
- if (updatedCount > 0) summary += `\n • ${updatedCount} module(s) updated with new sources`;
1667
- if (removedCount > 0) summary += `\n • ${removedCount} module(s) permanently deleted`;
1668
- await prompts.log.message(summary);
1669
- }
1670
-
1671
- return {
1672
- validCustomModules,
1673
- keptModulesWithoutSources,
1674
- };
1675
- }
1676
-
1677
1394
  /**
1678
1395
  * Find the bmad installation directory in a project
1679
1396
  * Always uses the standard _bmad folder name