bmad-method 6.0.0-alpha.17 → 6.0.0-alpha.18

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 (186) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/package.json +1 -1
  3. package/src/modules/bmgd/_module-installer/installer.js +160 -0
  4. package/src/modules/bmgd/_module-installer/platform-specifics/claude-code.js +23 -0
  5. package/src/modules/bmgd/_module-installer/platform-specifics/windsurf.js +18 -0
  6. package/src/modules/bmgd/agents/game-architect.agent.yaml +23 -8
  7. package/src/modules/bmgd/agents/game-designer.agent.yaml +38 -18
  8. package/src/modules/bmgd/agents/game-dev.agent.yaml +30 -14
  9. package/src/modules/bmgd/agents/game-qa.agent.yaml +64 -0
  10. package/src/modules/bmgd/agents/game-scrum-master.agent.yaml +27 -39
  11. package/src/modules/bmgd/agents/game-solo-dev.agent.yaml +56 -0
  12. package/src/modules/bmgd/docs/README.md +180 -0
  13. package/src/modules/bmgd/docs/agents-guide.md +407 -0
  14. package/src/modules/bmgd/docs/game-types-guide.md +503 -0
  15. package/src/modules/bmgd/docs/glossary.md +294 -0
  16. package/src/modules/bmgd/docs/quick-flow-guide.md +288 -0
  17. package/src/modules/bmgd/docs/quick-start.md +250 -0
  18. package/src/modules/bmgd/docs/troubleshooting.md +259 -0
  19. package/src/modules/bmgd/docs/workflow-overview.jpg +0 -0
  20. package/src/modules/bmgd/docs/workflows-guide.md +463 -0
  21. package/src/modules/bmgd/gametest/knowledge/balance-testing.md +220 -0
  22. package/src/modules/bmgd/gametest/knowledge/certification-testing.md +319 -0
  23. package/src/modules/bmgd/gametest/knowledge/compatibility-testing.md +228 -0
  24. package/src/modules/bmgd/gametest/knowledge/godot-testing.md +376 -0
  25. package/src/modules/bmgd/gametest/knowledge/input-testing.md +315 -0
  26. package/src/modules/bmgd/gametest/knowledge/localization-testing.md +304 -0
  27. package/src/modules/bmgd/gametest/knowledge/multiplayer-testing.md +322 -0
  28. package/src/modules/bmgd/gametest/knowledge/performance-testing.md +204 -0
  29. package/src/modules/bmgd/gametest/knowledge/playtesting.md +384 -0
  30. package/src/modules/bmgd/gametest/knowledge/qa-automation.md +190 -0
  31. package/src/modules/bmgd/gametest/knowledge/regression-testing.md +280 -0
  32. package/src/modules/bmgd/gametest/knowledge/save-testing.md +280 -0
  33. package/src/modules/bmgd/gametest/knowledge/smoke-testing.md +404 -0
  34. package/src/modules/bmgd/gametest/knowledge/test-priorities.md +271 -0
  35. package/src/modules/bmgd/gametest/knowledge/unity-testing.md +383 -0
  36. package/src/modules/bmgd/gametest/knowledge/unreal-testing.md +388 -0
  37. package/src/modules/bmgd/gametest/qa-index.csv +17 -0
  38. package/src/modules/bmgd/module.yaml +25 -9
  39. package/src/modules/bmgd/teams/default-party.csv +2 -0
  40. package/src/modules/bmgd/teams/team-gamedev.yaml +12 -1
  41. package/src/modules/bmgd/workflows/1-preproduction/brainstorm-game/steps/step-01-init.md +164 -0
  42. package/src/modules/bmgd/workflows/1-preproduction/brainstorm-game/steps/step-02-context.md +210 -0
  43. package/src/modules/bmgd/workflows/1-preproduction/brainstorm-game/steps/step-03-ideation.md +289 -0
  44. package/src/modules/bmgd/workflows/1-preproduction/brainstorm-game/steps/step-04-complete.md +275 -0
  45. package/src/modules/bmgd/workflows/1-preproduction/brainstorm-game/workflow.md +49 -0
  46. package/src/modules/bmgd/workflows/1-preproduction/brainstorm-game/workflow.yaml +29 -8
  47. package/src/modules/bmgd/workflows/1-preproduction/game-brief/steps/step-01-init.md +223 -0
  48. package/src/modules/bmgd/workflows/1-preproduction/game-brief/steps/step-01b-continue.md +151 -0
  49. package/src/modules/bmgd/workflows/1-preproduction/game-brief/steps/step-02-vision.md +218 -0
  50. package/src/modules/bmgd/workflows/1-preproduction/game-brief/steps/step-03-market.md +218 -0
  51. package/src/modules/bmgd/workflows/1-preproduction/game-brief/steps/step-04-fundamentals.md +231 -0
  52. package/src/modules/bmgd/workflows/1-preproduction/game-brief/steps/step-05-scope.md +242 -0
  53. package/src/modules/bmgd/workflows/1-preproduction/game-brief/steps/step-06-references.md +224 -0
  54. package/src/modules/bmgd/workflows/1-preproduction/game-brief/steps/step-07-content.md +282 -0
  55. package/src/modules/bmgd/workflows/1-preproduction/game-brief/steps/step-08-complete.md +296 -0
  56. package/src/modules/bmgd/workflows/1-preproduction/game-brief/workflow.md +62 -0
  57. package/src/modules/bmgd/workflows/1-preproduction/game-brief/workflow.yaml +40 -9
  58. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-01-init.md +248 -0
  59. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-01b-continue.md +173 -0
  60. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-02-context.md +332 -0
  61. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-03-platforms.md +245 -0
  62. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-04-vision.md +229 -0
  63. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-05-core-gameplay.md +258 -0
  64. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-06-mechanics.md +249 -0
  65. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-07-game-type.md +266 -0
  66. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-08-progression.md +272 -0
  67. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-09-levels.md +264 -0
  68. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-10-art-audio.md +255 -0
  69. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-11-technical.md +275 -0
  70. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-12-epics.md +284 -0
  71. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-13-metrics.md +250 -0
  72. package/src/modules/bmgd/workflows/2-design/gdd/steps/step-14-complete.md +335 -0
  73. package/src/modules/bmgd/workflows/2-design/gdd/workflow.md +61 -0
  74. package/src/modules/bmgd/workflows/2-design/gdd/workflow.yaml +27 -7
  75. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-01-init.md +228 -0
  76. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-01b-continue.md +163 -0
  77. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-02-foundation.md +262 -0
  78. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-03-story.md +238 -0
  79. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-04-characters.md +297 -0
  80. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-05-world.md +262 -0
  81. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-06-dialogue.md +250 -0
  82. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-07-environmental.md +244 -0
  83. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-08-delivery.md +264 -0
  84. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-09-integration.md +254 -0
  85. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-10-production.md +262 -0
  86. package/src/modules/bmgd/workflows/2-design/narrative/steps/step-11-complete.md +331 -0
  87. package/src/modules/bmgd/workflows/2-design/narrative/workflow.md +57 -0
  88. package/src/modules/bmgd/workflows/2-design/narrative/workflow.yaml +53 -8
  89. package/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-01-init.md +223 -0
  90. package/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-01b-continue.md +153 -0
  91. package/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-02-context.md +262 -0
  92. package/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-03-starter.md +290 -0
  93. package/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-04-decisions.md +300 -0
  94. package/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-05-crosscutting.md +319 -0
  95. package/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-06-structure.md +304 -0
  96. package/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-07-patterns.md +349 -0
  97. package/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-08-validation.md +293 -0
  98. package/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-09-complete.md +302 -0
  99. package/src/modules/bmgd/workflows/3-technical/game-architecture/workflow.md +55 -0
  100. package/src/modules/bmgd/workflows/3-technical/game-architecture/workflow.yaml +50 -21
  101. package/src/modules/bmgd/workflows/4-production/code-review/checklist.md +23 -0
  102. package/src/modules/bmgd/workflows/4-production/code-review/instructions.xml +225 -0
  103. package/src/modules/bmgd/workflows/4-production/code-review/workflow.yaml +18 -15
  104. package/src/modules/bmgd/workflows/4-production/correct-course/checklist.md +1 -1
  105. package/src/modules/bmgd/workflows/4-production/correct-course/instructions.md +1 -1
  106. package/src/modules/bmgd/workflows/4-production/correct-course/workflow.yaml +11 -6
  107. package/src/modules/bmgd/workflows/4-production/create-story/checklist.md +332 -214
  108. package/src/modules/bmgd/workflows/4-production/create-story/instructions.xml +298 -0
  109. package/src/modules/bmgd/workflows/4-production/create-story/template.md +3 -5
  110. package/src/modules/bmgd/workflows/4-production/create-story/workflow.yaml +12 -7
  111. package/src/modules/bmgd/workflows/4-production/dev-story/checklist.md +65 -23
  112. package/src/modules/bmgd/workflows/4-production/dev-story/instructions.xml +409 -0
  113. package/src/modules/bmgd/workflows/4-production/dev-story/workflow.yaml +13 -3
  114. package/src/modules/bmgd/workflows/4-production/retrospective/instructions.md +4 -4
  115. package/src/modules/bmgd/workflows/4-production/retrospective/workflow.yaml +12 -7
  116. package/src/modules/bmgd/workflows/4-production/sprint-planning/instructions.md +32 -41
  117. package/src/modules/bmgd/workflows/4-production/sprint-planning/sprint-status-template.yaml +13 -13
  118. package/src/modules/bmgd/workflows/4-production/sprint-planning/workflow.yaml +6 -1
  119. package/src/modules/bmgd/workflows/4-production/sprint-status/instructions.md +229 -0
  120. package/src/modules/bmgd/workflows/4-production/sprint-status/workflow.yaml +35 -0
  121. package/src/modules/bmgd/workflows/bmgd-quick-flow/create-tech-spec/instructions.md +140 -0
  122. package/src/modules/bmgd/workflows/bmgd-quick-flow/create-tech-spec/workflow.yaml +27 -0
  123. package/src/modules/bmgd/workflows/bmgd-quick-flow/quick-dev/checklist.md +37 -0
  124. package/src/modules/bmgd/workflows/bmgd-quick-flow/quick-dev/instructions.md +220 -0
  125. package/src/modules/bmgd/workflows/bmgd-quick-flow/quick-dev/workflow.yaml +45 -0
  126. package/src/modules/bmgd/workflows/bmgd-quick-flow/quick-prototype/checklist.md +26 -0
  127. package/src/modules/bmgd/workflows/bmgd-quick-flow/quick-prototype/instructions.md +156 -0
  128. package/src/modules/bmgd/workflows/bmgd-quick-flow/quick-prototype/workflow.yaml +36 -0
  129. package/src/modules/bmgd/workflows/gametest/automate/checklist.md +93 -0
  130. package/src/modules/bmgd/workflows/gametest/automate/instructions.md +317 -0
  131. package/src/modules/bmgd/workflows/gametest/automate/workflow.yaml +50 -0
  132. package/src/modules/bmgd/workflows/gametest/performance/checklist.md +96 -0
  133. package/src/modules/bmgd/workflows/gametest/performance/instructions.md +323 -0
  134. package/src/modules/bmgd/workflows/gametest/performance/performance-template.md +256 -0
  135. package/src/modules/bmgd/workflows/gametest/performance/workflow.yaml +48 -0
  136. package/src/modules/bmgd/workflows/gametest/playtest-plan/checklist.md +93 -0
  137. package/src/modules/bmgd/workflows/gametest/playtest-plan/instructions.md +297 -0
  138. package/src/modules/bmgd/workflows/gametest/playtest-plan/playtest-template.md +208 -0
  139. package/src/modules/bmgd/workflows/gametest/playtest-plan/workflow.yaml +59 -0
  140. package/src/modules/bmgd/workflows/gametest/test-design/checklist.md +98 -0
  141. package/src/modules/bmgd/workflows/gametest/test-design/instructions.md +280 -0
  142. package/src/modules/bmgd/workflows/gametest/test-design/test-design-template.md +205 -0
  143. package/src/modules/bmgd/workflows/gametest/test-design/workflow.yaml +47 -0
  144. package/src/modules/bmgd/workflows/gametest/test-framework/checklist.md +103 -0
  145. package/src/modules/bmgd/workflows/gametest/test-framework/instructions.md +348 -0
  146. package/src/modules/bmgd/workflows/gametest/test-framework/workflow.yaml +48 -0
  147. package/src/modules/bmgd/workflows/gametest/test-review/checklist.md +87 -0
  148. package/src/modules/bmgd/workflows/gametest/test-review/instructions.md +272 -0
  149. package/src/modules/bmgd/workflows/gametest/test-review/test-review-template.md +203 -0
  150. package/src/modules/bmgd/workflows/gametest/test-review/workflow.yaml +48 -0
  151. package/src/modules/bmgd/workflows/workflow-status/init/instructions.md +299 -0
  152. package/src/modules/bmgd/workflows/workflow-status/init/workflow.yaml +29 -0
  153. package/src/modules/bmgd/workflows/workflow-status/instructions.md +395 -0
  154. package/src/modules/bmgd/workflows/workflow-status/paths/gamedev-brownfield.yaml +65 -0
  155. package/src/modules/bmgd/workflows/workflow-status/paths/gamedev-greenfield.yaml +71 -0
  156. package/src/modules/bmgd/workflows/workflow-status/paths/quickflow-brownfield.yaml +29 -0
  157. package/src/modules/bmgd/workflows/workflow-status/paths/quickflow-greenfield.yaml +39 -0
  158. package/src/modules/bmgd/workflows/workflow-status/project-levels.yaml +63 -0
  159. package/src/modules/bmgd/workflows/workflow-status/workflow-status-template.yaml +24 -0
  160. package/src/modules/bmgd/workflows/workflow-status/workflow.yaml +30 -0
  161. package/tools/cli/commands/install.js +9 -0
  162. package/tools/cli/installers/lib/core/installer.js +109 -109
  163. package/tools/cli/installers/lib/core/installer.js.bak +3204 -0
  164. package/tools/cli/installers/lib/modules/manager.js +16 -4
  165. package/tools/cli/lib/agent/compiler.js +99 -0
  166. package/tools/cli/lib/ui.js +78 -27
  167. package/src/modules/bmgd/workflows/2-design/gdd/instructions-gdd.md +0 -502
  168. package/src/modules/bmgd/workflows/4-production/code-review/instructions.md +0 -398
  169. package/src/modules/bmgd/workflows/4-production/create-story/instructions.md +0 -256
  170. package/src/modules/bmgd/workflows/4-production/dev-story/instructions.md +0 -267
  171. package/src/modules/bmgd/workflows/4-production/epic-tech-context/checklist.md +0 -17
  172. package/src/modules/bmgd/workflows/4-production/epic-tech-context/instructions.md +0 -164
  173. package/src/modules/bmgd/workflows/4-production/epic-tech-context/template.md +0 -76
  174. package/src/modules/bmgd/workflows/4-production/epic-tech-context/workflow.yaml +0 -58
  175. package/src/modules/bmgd/workflows/4-production/story-context/checklist.md +0 -16
  176. package/src/modules/bmgd/workflows/4-production/story-context/context-template.xml +0 -34
  177. package/src/modules/bmgd/workflows/4-production/story-context/instructions.md +0 -209
  178. package/src/modules/bmgd/workflows/4-production/story-context/workflow.yaml +0 -63
  179. package/src/modules/bmgd/workflows/4-production/story-done/instructions.md +0 -111
  180. package/src/modules/bmgd/workflows/4-production/story-done/workflow.yaml +0 -28
  181. package/src/modules/bmgd/workflows/4-production/story-ready/instructions.md +0 -117
  182. package/src/modules/bmgd/workflows/4-production/story-ready/workflow.yaml +0 -25
  183. /package/src/modules/bmgd/workflows/1-preproduction/game-brief/{template.md → templates/game-brief-template.md} +0 -0
  184. /package/src/modules/bmgd/workflows/2-design/gdd/{gdd-template.md → templates/gdd-template.md} +0 -0
  185. /package/src/modules/bmgd/workflows/2-design/narrative/{narrative-template.md → templates/narrative-template.md} +0 -0
  186. /package/src/modules/bmgd/workflows/3-technical/game-architecture/{architecture-template.md → templates/architecture-template.md} +0 -0
@@ -0,0 +1,3204 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const inquirer = require('inquirer');
6
+ const { Detector } = require('./detector');
7
+ const { Manifest } = require('./manifest');
8
+ const { ModuleManager } = require('../modules/manager');
9
+ const { IdeManager } = require('../ide/manager');
10
+ const { FileOps } = require('../../../lib/file-ops');
11
+ const { Config } = require('../../../lib/config');
12
+ const { XmlHandler } = require('../../../lib/xml-handler');
13
+ const { DependencyResolver } = require('./dependency-resolver');
14
+ const { ConfigCollector } = require('./config-collector');
15
+ const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
16
+ const { AgentPartyGenerator } = require('../../../lib/agent-party-generator');
17
+ const { CLIUtils } = require('../../../lib/cli-utils');
18
+ const { ManifestGenerator } = require('./manifest-generator');
19
+ const { IdeConfigManager } = require('./ide-config-manager');
20
+ const { CustomHandler } = require('../custom/handler');
21
+ const { filterCustomizationData } = require('../../../lib/agent/compiler');
22
+
23
+ class Installer {
24
+ constructor() {
25
+ this.detector = new Detector();
26
+ this.manifest = new Manifest();
27
+ this.moduleManager = new ModuleManager();
28
+ this.ideManager = new IdeManager();
29
+ this.fileOps = new FileOps();
30
+ this.config = new Config();
31
+ this.xmlHandler = new XmlHandler();
32
+ this.dependencyResolver = new DependencyResolver();
33
+ this.configCollector = new ConfigCollector();
34
+ this.ideConfigManager = new IdeConfigManager();
35
+ this.installedFiles = new Set(); // Track all installed files
36
+ this.ttsInjectedFiles = []; // Track files with TTS injection applied
37
+ }
38
+
39
+ /**
40
+ * Find the bmad installation directory in a project
41
+ * V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml
42
+ * Also checks for legacy _cfg folder for migration
43
+ * @param {string} projectDir - Project directory
44
+ * @returns {Promise<Object>} { bmadDir: string, hasLegacyCfg: boolean }
45
+ */
46
+ async findBmadDir(projectDir) {
47
+ // Check if project directory exists
48
+ if (!(await fs.pathExists(projectDir))) {
49
+ // Project doesn't exist yet, return default
50
+ return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
51
+ }
52
+
53
+ let bmadDir = null;
54
+ let hasLegacyCfg = false;
55
+
56
+ try {
57
+ const entries = await fs.readdir(projectDir, { withFileTypes: true });
58
+ for (const entry of entries) {
59
+ if (entry.isDirectory()) {
60
+ const bmadPath = path.join(projectDir, entry.name);
61
+
62
+ // Check for current _config folder
63
+ const manifestPath = path.join(bmadPath, '_config', 'manifest.yaml');
64
+ if (await fs.pathExists(manifestPath)) {
65
+ // Found a V6+ installation with current _config folder
66
+ return { bmadDir: bmadPath, hasLegacyCfg: false };
67
+ }
68
+
69
+ // Check for legacy _cfg folder
70
+ const legacyManifestPath = path.join(bmadPath, '_cfg', 'manifest.yaml');
71
+ if (await fs.pathExists(legacyManifestPath)) {
72
+ bmadDir = bmadPath;
73
+ hasLegacyCfg = true;
74
+ }
75
+ }
76
+ }
77
+ } catch {
78
+ console.log(chalk.red('Error reading project directory for BMAD installation detection'));
79
+ }
80
+
81
+ // If we found a bmad directory (with or without legacy _cfg)
82
+ if (bmadDir) {
83
+ return { bmadDir, hasLegacyCfg };
84
+ }
85
+
86
+ // No V6+ installation found, return default
87
+ // This will be used for new installations
88
+ return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
89
+ }
90
+
91
+ /**
92
+ * @function copyFileWithPlaceholderReplacement
93
+ * @intent Copy files from BMAD source to installation directory with dynamic content transformation
94
+ * @why Enables installation-time customization: _bmad replacement + optional AgentVibes TTS injection
95
+ * @param {string} sourcePath - Absolute path to source file in BMAD repository
96
+ * @param {string} targetPath - Absolute path to destination file in user's project
97
+ * @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
98
+ * @returns {Promise<void>} Resolves when file copy and transformation complete
99
+ * @sideeffects Writes transformed file to targetPath, creates parent directories if needed
100
+ * @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails
101
+ * @calledby installCore(), installModule(), IDE installers during file vendoring
102
+ * @calls processTTSInjectionPoints(), fs.readFile(), fs.writeFile(), fs.copy()
103
+ *
104
+ * The injection point processing enables loose coupling between BMAD and TTS providers:
105
+ * - BMAD source contains injection markers (not actual TTS code)
106
+ * - At install-time, markers are replaced OR removed based on user preference
107
+ * - Result: Clean installs for users without TTS, working TTS for users with it
108
+ *
109
+ * PATTERN: Adding New Injection Points
110
+ * =====================================
111
+ * 1. Add HTML comment marker in BMAD source file:
112
+ * <!-- TTS_INJECTION:feature-name -->
113
+ *
114
+ * 2. Add replacement logic in processTTSInjectionPoints():
115
+ * if (enableAgentVibes) {
116
+ * content = content.replace(/<!-- TTS_INJECTION:feature-name -->/g, 'actual code');
117
+ * } else {
118
+ * content = content.replace(/<!-- TTS_INJECTION:feature-name -->\n?/g, '');
119
+ * }
120
+ *
121
+ * 3. Document marker in instructions.md (if applicable)
122
+ */
123
+ async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
124
+ // List of text file extensions that should have placeholder replacement
125
+ const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
126
+ const ext = path.extname(sourcePath).toLowerCase();
127
+
128
+ // Check if this is a text file that might contain placeholders
129
+ if (textExtensions.includes(ext)) {
130
+ try {
131
+ // Read the file content
132
+ let content = await fs.readFile(sourcePath, 'utf8');
133
+
134
+ // Process AgentVibes injection points (pass targetPath for tracking)
135
+ content = this.processTTSInjectionPoints(content, targetPath);
136
+
137
+ // Write to target with replaced content
138
+ await fs.ensureDir(path.dirname(targetPath));
139
+ await fs.writeFile(targetPath, content, 'utf8');
140
+ } catch {
141
+ // If reading as text fails (might be binary despite extension), fall back to regular copy
142
+ await fs.copy(sourcePath, targetPath, { overwrite: true });
143
+ }
144
+ } else {
145
+ // Binary file or other file type - just copy directly
146
+ await fs.copy(sourcePath, targetPath, { overwrite: true });
147
+ }
148
+ }
149
+
150
+ /**
151
+ * @function processTTSInjectionPoints
152
+ * @intent Transform TTS injection markers based on user's installation choice
153
+ * @why Enables optional TTS integration without tight coupling between BMAD and TTS providers
154
+ * @param {string} content - Raw file content containing potential injection markers
155
+ * @returns {string} Transformed content with markers replaced (if enabled) or stripped (if disabled)
156
+ * @sideeffects None - pure transformation function
157
+ * @edgecases Returns content unchanged if no markers present, safe to call on all files
158
+ * @calledby copyFileWithPlaceholderReplacement() during every file copy operation
159
+ * @calls String.replace() with regex patterns for each injection point type
160
+ *
161
+ * AI NOTE: This implements the injection point pattern for TTS integration.
162
+ * Key architectural decisions:
163
+ *
164
+ * 1. **Why Injection Points vs Direct Integration?**
165
+ * - BMAD and TTS providers are separate projects with different maintainers
166
+ * - Users may install BMAD without TTS support (and vice versa)
167
+ * - Hard-coding TTS calls would break BMAD for non-TTS users
168
+ * - Injection points allow conditional feature inclusion at install-time
169
+ *
170
+ * 2. **How It Works:**
171
+ * - BMAD source contains markers: <!-- TTS_INJECTION:feature-name -->
172
+ * - During installation, user is prompted: "Enable AgentVibes TTS?"
173
+ * - If YES: markers → replaced with actual bash TTS calls
174
+ * - If NO: markers → stripped cleanly from installed files
175
+ *
176
+ * 3. **State Management:**
177
+ * - this.enableAgentVibes set in install() method from config.enableAgentVibes
178
+ * - config.enableAgentVibes comes from ui.promptAgentVibes() user choice
179
+ * - Flag persists for entire installation, all files get same treatment
180
+ *
181
+ * CURRENT INJECTION POINTS:
182
+ * ==========================
183
+ * - party-mode: Injects TTS calls after each agent speaks in party mode
184
+ * Location: src/core/workflows/party-mode/instructions.md
185
+ * Marker: <!-- TTS_INJECTION:party-mode -->
186
+ * Replacement: Bash call to .claude/hooks/bmad-speak.sh with agent name and dialogue
187
+ *
188
+ * - agent-tts: Injects TTS rule for individual agent conversations
189
+ * Location: src/modules/bmm/agents/*.md (all agent files)
190
+ * Marker: <!-- TTS_INJECTION:agent-tts -->
191
+ * Replacement: Rule instructing agent to call bmad-speak.sh with agent ID and response
192
+ *
193
+ * ADDING NEW INJECTION POINTS:
194
+ * =============================
195
+ * 1. Add new case in this function:
196
+ * content = content.replace(
197
+ * /<!-- TTS_INJECTION:new-feature -->/g,
198
+ * `code to inject when enabled`
199
+ * );
200
+ *
201
+ * 2. Add marker to BMAD source file at injection location
202
+ *
203
+ * 3. Test both enabled and disabled flows
204
+ *
205
+ * RELATED:
206
+ * ========
207
+ * - GitHub Issue: paulpreibisch/AgentVibes#36
208
+ * - User Prompt: tools/cli/lib/ui.js::promptAgentVibes()
209
+ * - Marker Locations:
210
+ * - src/core/workflows/party-mode/instructions.md:101
211
+ * - src/modules/bmm/agents/*.md (rules sections)
212
+ * - TTS Hook: .claude/hooks/bmad-speak.sh (in AgentVibes repo)
213
+ */
214
+ processTTSInjectionPoints(content, targetPath = null) {
215
+ // Check if AgentVibes is enabled (set during installation configuration)
216
+ const enableAgentVibes = this.enableAgentVibes || false;
217
+
218
+ // Check if content contains any TTS injection markers
219
+ const hasPartyMode = content.includes('<!-- TTS_INJECTION:party-mode -->');
220
+ const hasAgentTTS = content.includes('<!-- TTS_INJECTION:agent-tts -->');
221
+
222
+ if (enableAgentVibes) {
223
+ // Replace party-mode injection marker with actual TTS call
224
+ // Use single quotes to prevent shell expansion of special chars like !
225
+ content = content.replaceAll(
226
+ '<!-- TTS_INJECTION:party-mode -->',
227
+ `<critical>IMPORTANT: Always use PROJECT hooks (.claude/hooks/), NEVER global hooks (~/.claude/hooks/)</critical>
228
+
229
+ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
230
+ - Use Bash tool: \`.claude/hooks/bmad-speak.sh '[Agent Name]' '[dialogue]'\`
231
+ - This speaks the dialogue with the agent's unique voice
232
+ - Run in background to not block next agent`,
233
+ );
234
+
235
+ // Replace agent-tts injection marker with TTS rule for individual agents
236
+ content = content.replaceAll(
237
+ '<!-- TTS_INJECTION:agent-tts -->',
238
+ `- When responding to user messages, speak your responses using TTS:
239
+ Call: \`.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'\` after each response
240
+ Replace {agent-id} with YOUR agent ID from <agent id="..."> tag at top of this file
241
+ Replace {response-text} with the text you just output to the user
242
+ IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
243
+ Run in background (&) to avoid blocking`,
244
+ );
245
+
246
+ // Track files that had TTS injection applied
247
+ if (targetPath && (hasPartyMode || hasAgentTTS)) {
248
+ const injectionType = hasPartyMode ? 'party-mode' : 'agent-tts';
249
+ this.ttsInjectedFiles.push({ path: targetPath, type: injectionType });
250
+ }
251
+ } else {
252
+ // Strip injection markers cleanly when AgentVibes is disabled
253
+ content = content.replaceAll(/<!-- TTS_INJECTION:party-mode -->\n?/g, '');
254
+ content = content.replaceAll(/<!-- TTS_INJECTION:agent-tts -->\n?/g, '');
255
+ }
256
+
257
+ return content;
258
+ }
259
+
260
+ /**
261
+ * Collect Tool/IDE configurations after module configuration
262
+ * @param {string} projectDir - Project directory
263
+ * @param {Array} selectedModules - Selected modules from configuration
264
+ * @param {boolean} isFullReinstall - Whether this is a full reinstall
265
+ * @param {Array} previousIdes - Previously configured IDEs (for reinstalls)
266
+ * @param {Array} preSelectedIdes - Pre-selected IDEs from early prompt (optional)
267
+ * @returns {Object} Tool/IDE selection and configurations
268
+ */
269
+ async collectToolConfigurations(projectDir, selectedModules, isFullReinstall = false, previousIdes = [], preSelectedIdes = null) {
270
+ // Use pre-selected IDEs if provided, otherwise prompt
271
+ let toolConfig;
272
+ if (preSelectedIdes === null) {
273
+ // Fallback: prompt for tool selection (backwards compatibility)
274
+ const { UI } = require('../../../lib/ui');
275
+ const ui = new UI();
276
+ toolConfig = await ui.promptToolSelection(projectDir, selectedModules);
277
+ } else {
278
+ // IDEs were already selected during initial prompts
279
+ toolConfig = {
280
+ ides: preSelectedIdes,
281
+ skipIde: !preSelectedIdes || preSelectedIdes.length === 0,
282
+ };
283
+ }
284
+
285
+ // Check for already configured IDEs
286
+ const { Detector } = require('./detector');
287
+ const detector = new Detector();
288
+ const bmadDir = path.join(projectDir, this.bmadFolderName || 'bmad');
289
+
290
+ // During full reinstall, use the saved previous IDEs since bmad dir was deleted
291
+ // Otherwise detect from existing installation
292
+ let previouslyConfiguredIdes;
293
+ if (isFullReinstall) {
294
+ // During reinstall, treat all IDEs as new (need configuration)
295
+ previouslyConfiguredIdes = [];
296
+ } else {
297
+ const existingInstall = await detector.detect(bmadDir);
298
+ previouslyConfiguredIdes = existingInstall.ides || [];
299
+ }
300
+
301
+ // Load saved IDE configurations for already-configured IDEs
302
+ const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
303
+
304
+ // Collect IDE-specific configurations if any were selected
305
+ const ideConfigurations = {};
306
+
307
+ // First, add saved configs for already-configured IDEs
308
+ for (const ide of toolConfig.ides || []) {
309
+ if (previouslyConfiguredIdes.includes(ide) && savedIdeConfigs[ide]) {
310
+ ideConfigurations[ide] = savedIdeConfigs[ide];
311
+ }
312
+ }
313
+
314
+ if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
315
+ // Determine which IDEs are newly selected (not previously configured)
316
+ const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide));
317
+
318
+ if (newlySelectedIdes.length > 0) {
319
+ console.log('\n'); // Add spacing before IDE questions
320
+
321
+ for (const ide of newlySelectedIdes) {
322
+ // List of IDEs that have interactive prompts
323
+ //TODO: Why is this here, hardcoding this list here is bad, fix me!
324
+ const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini', 'rovo-dev'].includes(
325
+ ide,
326
+ );
327
+
328
+ if (needsPrompts) {
329
+ // Get IDE handler and collect configuration
330
+ try {
331
+ // Dynamically load the IDE setup module
332
+ const ideModule = require(`../ide/${ide}`);
333
+
334
+ // Get the setup class (handle different export formats)
335
+ let SetupClass;
336
+ const className =
337
+ ide
338
+ .split('-')
339
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
340
+ .join('') + 'Setup';
341
+
342
+ if (ideModule[className]) {
343
+ SetupClass = ideModule[className];
344
+ } else if (ideModule.default) {
345
+ SetupClass = ideModule.default;
346
+ } else {
347
+ continue;
348
+ }
349
+
350
+ const ideSetup = new SetupClass();
351
+
352
+ // Check if this IDE has a collectConfiguration method
353
+ if (typeof ideSetup.collectConfiguration === 'function') {
354
+ console.log(chalk.cyan(`\nConfiguring ${ide}...`));
355
+ ideConfigurations[ide] = await ideSetup.collectConfiguration({
356
+ selectedModules: selectedModules || [],
357
+ projectDir,
358
+ bmadDir,
359
+ });
360
+ }
361
+ } catch {
362
+ // IDE doesn't have a setup file or collectConfiguration method
363
+ console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}`));
364
+ }
365
+ }
366
+ }
367
+ }
368
+
369
+ // Log which IDEs are already configured and being kept
370
+ const keptIdes = toolConfig.ides.filter((ide) => previouslyConfiguredIdes.includes(ide));
371
+ if (keptIdes.length > 0) {
372
+ console.log(chalk.dim(`\nKeeping existing configuration for: ${keptIdes.join(', ')}`));
373
+ }
374
+ }
375
+
376
+ return {
377
+ ides: toolConfig.ides,
378
+ skipIde: toolConfig.skipIde,
379
+ configurations: ideConfigurations,
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Main installation method
385
+ * @param {Object} config - Installation configuration
386
+ * @param {string} config.directory - Target directory
387
+ * @param {boolean} config.installCore - Whether to install core
388
+ * @param {string[]} config.modules - Modules to install
389
+ * @param {string[]} config.ides - IDEs to configure
390
+ * @param {boolean} config.skipIde - Skip IDE configuration
391
+ */
392
+ async install(originalConfig) {
393
+ // Clone config to avoid mutating the caller's object
394
+ const config = { ...originalConfig };
395
+
396
+ // Check if core config was already collected in UI
397
+ const hasCoreConfig = config.coreConfig && Object.keys(config.coreConfig).length > 0;
398
+
399
+ // Only display logo if core config wasn't already collected (meaning we're not continuing from UI)
400
+ if (!hasCoreConfig) {
401
+ // Display BMAD logo
402
+ CLIUtils.displayLogo();
403
+
404
+ // Display welcome message
405
+ CLIUtils.displaySection('BMad™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version);
406
+ }
407
+
408
+ // Note: Legacy V4 detection now happens earlier in UI.promptInstall()
409
+ // before any config collection, so we don't need to check again here
410
+
411
+ const projectDir = path.resolve(config.directory);
412
+
413
+ // If core config was pre-collected (from interactive mode), use it
414
+ if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
415
+ this.configCollector.collectedConfig.core = config.coreConfig;
416
+ // Also store in allAnswers for cross-referencing
417
+ this.configCollector.allAnswers = {};
418
+ for (const [key, value] of Object.entries(config.coreConfig)) {
419
+ this.configCollector.allAnswers[`core_${key}`] = value;
420
+ }
421
+ }
422
+
423
+ // Collect configurations for modules (skip if quick update already collected them)
424
+ let moduleConfigs;
425
+ let customModulePaths = new Map();
426
+
427
+ if (config._quickUpdate) {
428
+ // Quick update already collected all configs, use them directly
429
+ moduleConfigs = this.configCollector.collectedConfig;
430
+
431
+ // For quick update, populate customModulePaths from _customModuleSources
432
+ if (config._customModuleSources) {
433
+ for (const [moduleId, customInfo] of config._customModuleSources) {
434
+ customModulePaths.set(moduleId, customInfo.sourcePath);
435
+ }
436
+ }
437
+ } else {
438
+ // For regular updates (modify flow), check manifest for custom module sources
439
+ if (config._isUpdate && config._existingInstall && config._existingInstall.customModules) {
440
+ for (const customModule of config._existingInstall.customModules) {
441
+ // Ensure we have an absolute sourcePath
442
+ let absoluteSourcePath = customModule.sourcePath;
443
+
444
+ // Check if sourcePath is a cache-relative path (starts with _config)
445
+ if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
446
+ // Convert cache-relative path to absolute path
447
+ absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
448
+ }
449
+ // If no sourcePath but we have relativePath, convert it
450
+ else if (!absoluteSourcePath && customModule.relativePath) {
451
+ // relativePath is relative to the project root (parent of bmad dir)
452
+ absoluteSourcePath = path.resolve(projectDir, customModule.relativePath);
453
+ }
454
+ // Ensure sourcePath is absolute for anything else
455
+ else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
456
+ absoluteSourcePath = path.resolve(absoluteSourcePath);
457
+ }
458
+
459
+ if (absoluteSourcePath) {
460
+ customModulePaths.set(customModule.id, absoluteSourcePath);
461
+ }
462
+ }
463
+ }
464
+
465
+ // Build custom module paths map from customContent
466
+
467
+ // Handle selectedFiles (from existing install path or manual directory input)
468
+ if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
469
+ const customHandler = new CustomHandler();
470
+ for (const customFile of config.customContent.selectedFiles) {
471
+ const customInfo = await customHandler.getCustomInfo(customFile, path.resolve(config.directory));
472
+ if (customInfo && customInfo.id) {
473
+ customModulePaths.set(customInfo.id, customInfo.path);
474
+ }
475
+ }
476
+ }
477
+
478
+ // Handle new custom content sources from UI
479
+ if (config.customContent && config.customContent.sources) {
480
+ for (const source of config.customContent.sources) {
481
+ customModulePaths.set(source.id, source.path);
482
+ }
483
+ }
484
+
485
+ // Handle cachedModules (from new install path where modules are cached)
486
+ // Only include modules that were actually selected for installation
487
+ if (config.customContent && config.customContent.cachedModules) {
488
+ // Get selected cached module IDs (if available)
489
+ const selectedCachedIds = config.customContent.selectedCachedModules || [];
490
+ // If no selection info, include all cached modules (for backward compatibility)
491
+ const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected;
492
+
493
+ for (const cachedModule of config.customContent.cachedModules) {
494
+ // For cached modules, the path is the cachePath which contains the module.yaml
495
+ if (
496
+ cachedModule.id &&
497
+ cachedModule.cachePath && // Include if selected or if we should include all
498
+ (shouldIncludeAll || selectedCachedIds.includes(cachedModule.id))
499
+ ) {
500
+ customModulePaths.set(cachedModule.id, cachedModule.cachePath);
501
+ }
502
+ }
503
+ }
504
+
505
+ // Get list of all modules including custom modules
506
+ // Order: core first, then official modules, then custom modules
507
+ const allModulesForConfig = ['core'];
508
+
509
+ // Add official modules (excluding core and any custom modules)
510
+ const officialModules = (config.modules || []).filter((m) => m !== 'core' && !customModulePaths.has(m));
511
+ allModulesForConfig.push(...officialModules);
512
+
513
+ // Add custom modules at the end
514
+ for (const [moduleId] of customModulePaths) {
515
+ if (!allModulesForConfig.includes(moduleId)) {
516
+ allModulesForConfig.push(moduleId);
517
+ }
518
+ }
519
+
520
+ // Check if core was already collected in UI
521
+ if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
522
+ // Core already collected, skip it in config collection
523
+ const modulesWithoutCore = allModulesForConfig.filter((m) => m !== 'core');
524
+ moduleConfigs = await this.configCollector.collectAllConfigurations(modulesWithoutCore, path.resolve(config.directory), {
525
+ customModulePaths,
526
+ });
527
+ } else {
528
+ // Core not collected yet, include it
529
+ moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), {
530
+ customModulePaths,
531
+ });
532
+ }
533
+ }
534
+
535
+ // Always use _bmad as the folder name
536
+ const bmadFolderName = '_bmad';
537
+ this.bmadFolderName = bmadFolderName; // Store for use in other methods
538
+
539
+ // Store AgentVibes configuration for injection point processing
540
+ this.enableAgentVibes = config.enableAgentVibes || false;
541
+
542
+ // Set bmad folder name on module manager and IDE manager for placeholder replacement
543
+ this.moduleManager.setBmadFolderName(bmadFolderName);
544
+ this.moduleManager.setCoreConfig(moduleConfigs.core || {});
545
+ this.moduleManager.setCustomModulePaths(customModulePaths);
546
+ this.ideManager.setBmadFolderName(bmadFolderName);
547
+
548
+ // Tool selection will be collected after we determine if it's a reinstall/update/new install
549
+
550
+ const spinner = ora('Preparing installation...').start();
551
+
552
+ try {
553
+ // Resolve target directory (path.resolve handles platform differences)
554
+ const projectDir = path.resolve(config.directory);
555
+
556
+ let existingBmadDir = null;
557
+ let existingBmadFolderName = null;
558
+
559
+ if (await fs.pathExists(projectDir)) {
560
+ const result = await this.findBmadDir(projectDir);
561
+ existingBmadDir = result.bmadDir;
562
+ existingBmadFolderName = path.basename(existingBmadDir);
563
+ }
564
+
565
+ // Create a project directory if it doesn't exist (user already confirmed)
566
+ if (!(await fs.pathExists(projectDir))) {
567
+ spinner.text = 'Creating installation directory...';
568
+ try {
569
+ // fs.ensureDir handles platform-specific directory creation
570
+ // It will recursively create all necessary parent directories
571
+ await fs.ensureDir(projectDir);
572
+ } catch (error) {
573
+ spinner.fail('Failed to create installation directory');
574
+ console.error(chalk.red(`Error: ${error.message}`));
575
+ // More detailed error for common issues
576
+ if (error.code === 'EACCES') {
577
+ console.error(chalk.red('Permission denied. Check parent directory permissions.'));
578
+ } else if (error.code === 'ENOSPC') {
579
+ console.error(chalk.red('No space left on device.'));
580
+ }
581
+ throw new Error(`Cannot create directory: ${projectDir}`);
582
+ }
583
+ }
584
+
585
+ const bmadDir = path.join(projectDir, bmadFolderName);
586
+
587
+ // Check existing installation
588
+ spinner.text = 'Checking for existing installation...';
589
+ const existingInstall = await this.detector.detect(bmadDir);
590
+
591
+ if (existingInstall.installed && !config.force && !config._quickUpdate) {
592
+ spinner.stop();
593
+
594
+ // Check if user already decided what to do (from early menu in ui.js)
595
+ let action = null;
596
+ if (config.actionType === 'update') {
597
+ action = 'update';
598
+ } else {
599
+ // Fallback: Ask the user (backwards compatibility for other code paths)
600
+ console.log(chalk.yellow('\n⚠️ Existing BMAD installation detected'));
601
+ console.log(chalk.dim(` Location: ${bmadDir}`));
602
+ console.log(chalk.dim(` Version: ${existingInstall.version}`));
603
+
604
+ const promptResult = await this.promptUpdateAction();
605
+ action = promptResult.action;
606
+ }
607
+
608
+ if (action === 'update') {
609
+ // Store that we're updating for later processing
610
+ config._isUpdate = true;
611
+ config._existingInstall = existingInstall;
612
+
613
+ // Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
614
+ const existingFilesManifest = await this.readFilesManifest(bmadDir);
615
+ const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
616
+
617
+ config._customFiles = customFiles;
618
+ config._modifiedFiles = modifiedFiles;
619
+
620
+ // Also check cache directory for custom modules (like quick update does)
621
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
622
+ if (await fs.pathExists(cacheDir)) {
623
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
624
+
625
+ for (const cachedModule of cachedModules) {
626
+ if (cachedModule.isDirectory()) {
627
+ const moduleId = cachedModule.name;
628
+
629
+ // Skip if we already have this module from manifest
630
+ if (customModulePaths.has(moduleId)) {
631
+ continue;
632
+ }
633
+
634
+ const cachedPath = path.join(cacheDir, moduleId);
635
+
636
+ // Check if this is actually a custom module (has module.yaml)
637
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
638
+ if (await fs.pathExists(moduleYamlPath)) {
639
+ customModulePaths.set(moduleId, cachedPath);
640
+ }
641
+ }
642
+ }
643
+
644
+ // Update module manager with the new custom module paths from cache
645
+ this.moduleManager.setCustomModulePaths(customModulePaths);
646
+ }
647
+
648
+ // If there are custom files, back them up temporarily
649
+ if (customFiles.length > 0) {
650
+ const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
651
+ await fs.ensureDir(tempBackupDir);
652
+
653
+ spinner.start(`Backing up ${customFiles.length} custom files...`);
654
+ for (const customFile of customFiles) {
655
+ const relativePath = path.relative(bmadDir, customFile);
656
+ const backupPath = path.join(tempBackupDir, relativePath);
657
+ await fs.ensureDir(path.dirname(backupPath));
658
+ await fs.copy(customFile, backupPath);
659
+ }
660
+ spinner.succeed(`Backed up ${customFiles.length} custom files`);
661
+
662
+ config._tempBackupDir = tempBackupDir;
663
+ }
664
+
665
+ // For modified files, back them up to temp directory (will be restored as .bak files after install)
666
+ if (modifiedFiles.length > 0) {
667
+ const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
668
+ await fs.ensureDir(tempModifiedBackupDir);
669
+
670
+ spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
671
+ for (const modifiedFile of modifiedFiles) {
672
+ const relativePath = path.relative(bmadDir, modifiedFile.path);
673
+ const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
674
+ await fs.ensureDir(path.dirname(tempBackupPath));
675
+ await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
676
+ }
677
+ spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
678
+
679
+ config._tempModifiedBackupDir = tempModifiedBackupDir;
680
+ }
681
+ }
682
+ } else if (existingInstall.installed && config._quickUpdate) {
683
+ // Quick update mode - automatically treat as update without prompting
684
+ spinner.text = 'Preparing quick update...';
685
+ config._isUpdate = true;
686
+ config._existingInstall = existingInstall;
687
+
688
+ // Detect custom and modified files BEFORE updating
689
+ const existingFilesManifest = await this.readFilesManifest(bmadDir);
690
+ const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
691
+
692
+ config._customFiles = customFiles;
693
+ config._modifiedFiles = modifiedFiles;
694
+
695
+ // Also check cache directory for custom modules (like quick update does)
696
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
697
+ if (await fs.pathExists(cacheDir)) {
698
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
699
+
700
+ for (const cachedModule of cachedModules) {
701
+ if (cachedModule.isDirectory()) {
702
+ const moduleId = cachedModule.name;
703
+
704
+ // Skip if we already have this module from manifest
705
+ if (customModulePaths.has(moduleId)) {
706
+ continue;
707
+ }
708
+
709
+ const cachedPath = path.join(cacheDir, moduleId);
710
+
711
+ // Check if this is actually a custom module (has module.yaml)
712
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
713
+ if (await fs.pathExists(moduleYamlPath)) {
714
+ customModulePaths.set(moduleId, cachedPath);
715
+ }
716
+ }
717
+ }
718
+
719
+ // Update module manager with the new custom module paths from cache
720
+ this.moduleManager.setCustomModulePaths(customModulePaths);
721
+ }
722
+
723
+ // Back up custom files
724
+ if (customFiles.length > 0) {
725
+ const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
726
+ await fs.ensureDir(tempBackupDir);
727
+
728
+ spinner.start(`Backing up ${customFiles.length} custom files...`);
729
+ for (const customFile of customFiles) {
730
+ const relativePath = path.relative(bmadDir, customFile);
731
+ const backupPath = path.join(tempBackupDir, relativePath);
732
+ await fs.ensureDir(path.dirname(backupPath));
733
+ await fs.copy(customFile, backupPath);
734
+ }
735
+ spinner.succeed(`Backed up ${customFiles.length} custom files`);
736
+ config._tempBackupDir = tempBackupDir;
737
+ }
738
+
739
+ // Back up modified files
740
+ if (modifiedFiles.length > 0) {
741
+ const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
742
+ await fs.ensureDir(tempModifiedBackupDir);
743
+
744
+ spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
745
+ for (const modifiedFile of modifiedFiles) {
746
+ const relativePath = path.relative(bmadDir, modifiedFile.path);
747
+ const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
748
+ await fs.ensureDir(path.dirname(tempBackupPath));
749
+ await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
750
+ }
751
+ spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
752
+ config._tempModifiedBackupDir = tempModifiedBackupDir;
753
+ }
754
+ }
755
+
756
+ // Now collect tool configurations after we know if it's a reinstall
757
+ // Skip for quick update since we already have the IDE list
758
+ spinner.stop();
759
+ let toolSelection;
760
+ if (config._quickUpdate) {
761
+ // Quick update already has IDEs configured, use saved configurations
762
+ const preConfiguredIdes = {};
763
+ const savedIdeConfigs = config._savedIdeConfigs || {};
764
+
765
+ for (const ide of config.ides || []) {
766
+ // Use saved config if available, otherwise mark as already configured (legacy)
767
+ if (savedIdeConfigs[ide]) {
768
+ preConfiguredIdes[ide] = savedIdeConfigs[ide];
769
+ } else {
770
+ preConfiguredIdes[ide] = { _alreadyConfigured: true };
771
+ }
772
+ }
773
+ toolSelection = {
774
+ ides: config.ides || [],
775
+ skipIde: !config.ides || config.ides.length === 0,
776
+ configurations: preConfiguredIdes,
777
+ };
778
+ } else {
779
+ // Pass pre-selected IDEs from early prompt (if available)
780
+ // This allows IDE selection to happen before file copying, improving UX
781
+ const preSelectedIdes = config.ides && config.ides.length > 0 ? config.ides : null;
782
+ toolSelection = await this.collectToolConfigurations(
783
+ path.resolve(config.directory),
784
+ config.modules,
785
+ config._isFullReinstall || false,
786
+ config._previouslyConfiguredIdes || [],
787
+ preSelectedIdes,
788
+ );
789
+ }
790
+
791
+ // Merge tool selection into config (for both quick update and regular flow)
792
+ config.ides = toolSelection.ides;
793
+ config.skipIde = toolSelection.skipIde;
794
+ const ideConfigurations = toolSelection.configurations;
795
+
796
+ if (spinner.isSpinning) {
797
+ spinner.text = 'Continuing installation...';
798
+ } else {
799
+ spinner.start('Continuing installation...');
800
+ }
801
+
802
+ // Create bmad directory structure
803
+ spinner.text = 'Creating directory structure...';
804
+ await this.createDirectoryStructure(bmadDir);
805
+
806
+ // Cache custom modules if any
807
+ if (customModulePaths && customModulePaths.size > 0) {
808
+ spinner.text = 'Caching custom modules...';
809
+ const { CustomModuleCache } = require('./custom-module-cache');
810
+ const customCache = new CustomModuleCache(bmadDir);
811
+
812
+ for (const [moduleId, sourcePath] of customModulePaths) {
813
+ const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
814
+ sourcePath: sourcePath, // Store original path for updates
815
+ });
816
+
817
+ // Update the customModulePaths to use the cached location
818
+ customModulePaths.set(moduleId, cachedInfo.cachePath);
819
+ }
820
+
821
+ // Update module manager with the cached paths
822
+ this.moduleManager.setCustomModulePaths(customModulePaths);
823
+ spinner.succeed('Custom modules cached');
824
+ }
825
+
826
+ const projectRoot = getProjectRoot();
827
+
828
+ // Step 1: Install core module first (if requested)
829
+ if (config.installCore) {
830
+ spinner.start('Installing BMAD core...');
831
+ await this.installCoreWithDependencies(bmadDir, { core: {} });
832
+ spinner.succeed('Core installed');
833
+
834
+ // Generate core config file
835
+ await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
836
+ }
837
+
838
+ // Custom content is already handled in UI before module selection
839
+ let finalCustomContent = config.customContent;
840
+
841
+ // Step 3: Prepare modules list including cached custom modules
842
+ let allModules = [...(config.modules || [])];
843
+
844
+ // During quick update, we might have custom module sources from the manifest
845
+ if (config._customModuleSources) {
846
+ // Add custom modules from stored sources
847
+ for (const [moduleId, customInfo] of config._customModuleSources) {
848
+ if (!allModules.includes(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
849
+ allModules.push(moduleId);
850
+ }
851
+ }
852
+ }
853
+
854
+ // Add cached custom modules
855
+ if (finalCustomContent && finalCustomContent.cachedModules) {
856
+ for (const cachedModule of finalCustomContent.cachedModules) {
857
+ if (!allModules.includes(cachedModule.id)) {
858
+ allModules.push(cachedModule.id);
859
+ }
860
+ }
861
+ }
862
+
863
+ // Regular custom content from user input (non-cached)
864
+ if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
865
+ // Add custom modules to the installation list
866
+ const customHandler = new CustomHandler();
867
+ for (const customFile of finalCustomContent.selectedFiles) {
868
+ const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
869
+ if (customInfo && customInfo.id) {
870
+ allModules.push(customInfo.id);
871
+ }
872
+ }
873
+ }
874
+
875
+ // Don't include core again if already installed
876
+ if (config.installCore) {
877
+ allModules = allModules.filter((m) => m !== 'core');
878
+ }
879
+
880
+ const modulesToInstall = allModules;
881
+
882
+ // For dependency resolution, we only need regular modules (not custom modules)
883
+ // Custom modules are already installed in _bmad and don't need dependency resolution from source
884
+ const regularModulesForResolution = allModules.filter((module) => {
885
+ // Check if this is a custom module
886
+ const isCustom =
887
+ customModulePaths.has(module) ||
888
+ (finalCustomContent && finalCustomContent.cachedModules && finalCustomContent.cachedModules.some((cm) => cm.id === module)) ||
889
+ (finalCustomContent &&
890
+ finalCustomContent.selected &&
891
+ finalCustomContent.selectedFiles &&
892
+ finalCustomContent.selectedFiles.some((f) => f.includes(module)));
893
+ return !isCustom;
894
+ });
895
+
896
+ // For dependency resolution, we need to pass the project root
897
+ // Create a temporary module manager that knows about custom content locations
898
+ const tempModuleManager = new ModuleManager({
899
+ bmadDir: bmadDir, // Pass bmadDir so we can check cache
900
+ });
901
+
902
+ const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
903
+ verbose: config.verbose,
904
+ moduleManager: tempModuleManager,
905
+ });
906
+
907
+ spinner.succeed('Dependencies resolved');
908
+
909
+ // Install modules with their dependencies
910
+ if (allModules && allModules.length > 0) {
911
+ const installedModuleNames = new Set();
912
+
913
+ for (const moduleName of allModules) {
914
+ // Skip if already installed
915
+ if (installedModuleNames.has(moduleName)) {
916
+ continue;
917
+ }
918
+ installedModuleNames.add(moduleName);
919
+
920
+ // Show appropriate message based on whether this is a quick update
921
+ const isQuickUpdate = config._quickUpdate || false;
922
+ spinner.start(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`);
923
+
924
+ // Check if this is a custom module
925
+ let isCustomModule = false;
926
+ let customInfo = null;
927
+ let useCache = false;
928
+
929
+ // First check if we have a cached version
930
+ if (finalCustomContent && finalCustomContent.cachedModules) {
931
+ const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
932
+ if (cachedModule) {
933
+ isCustomModule = true;
934
+ customInfo = {
935
+ id: moduleName,
936
+ path: cachedModule.cachePath,
937
+ config: {},
938
+ };
939
+ useCache = true;
940
+ }
941
+ }
942
+
943
+ // Then check if we have custom module sources from the manifest (for quick update)
944
+ if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
945
+ customInfo = config._customModuleSources.get(moduleName);
946
+ isCustomModule = true;
947
+
948
+ // Check if this is a cached module (source path starts with _config)
949
+ if (
950
+ customInfo.sourcePath &&
951
+ (customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom'))
952
+ ) {
953
+ useCache = true;
954
+ // Make sure we have the right path structure
955
+ if (!customInfo.path) {
956
+ customInfo.path = customInfo.sourcePath;
957
+ }
958
+ }
959
+ }
960
+
961
+ // Finally check regular custom content
962
+ if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
963
+ const customHandler = new CustomHandler();
964
+ for (const customFile of finalCustomContent.selectedFiles) {
965
+ const info = await customHandler.getCustomInfo(customFile, projectDir);
966
+ if (info && info.id === moduleName) {
967
+ isCustomModule = true;
968
+ customInfo = info;
969
+ break;
970
+ }
971
+ }
972
+ }
973
+
974
+ if (isCustomModule && customInfo) {
975
+ // Custom modules are now installed via ModuleManager just like standard modules
976
+ // The custom module path should already be in customModulePaths from earlier setup
977
+ if (!customModulePaths.has(moduleName) && customInfo.path) {
978
+ customModulePaths.set(moduleName, customInfo.path);
979
+ this.moduleManager.setCustomModulePaths(customModulePaths);
980
+ }
981
+
982
+ const collectedModuleConfig = moduleConfigs[moduleName] || {};
983
+
984
+ // Use ModuleManager to install the custom module
985
+ await this.moduleManager.install(
986
+ moduleName,
987
+ bmadDir,
988
+ (filePath) => {
989
+ this.installedFiles.add(filePath);
990
+ },
991
+ {
992
+ isCustom: true,
993
+ moduleConfig: collectedModuleConfig,
994
+ isQuickUpdate: config._quickUpdate || false,
995
+ installer: this,
996
+ },
997
+ );
998
+
999
+ // Create module config (include collected config from module.yaml prompts)
1000
+ await this.generateModuleConfigs(bmadDir, {
1001
+ [moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
1002
+ });
1003
+ } else {
1004
+ // Regular module installation
1005
+ // Special case for core module
1006
+ if (moduleName === 'core') {
1007
+ await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
1008
+ } else {
1009
+ await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
1010
+ }
1011
+ }
1012
+
1013
+ spinner.succeed(`Module ${isQuickUpdate ? 'updated' : 'installed'}: ${moduleName}`);
1014
+ }
1015
+
1016
+ // Install partial modules (only dependencies)
1017
+ for (const [module, files] of Object.entries(resolution.byModule)) {
1018
+ if (!allModules.includes(module) && module !== 'core') {
1019
+ const totalFiles =
1020
+ files.agents.length +
1021
+ files.tasks.length +
1022
+ files.tools.length +
1023
+ files.templates.length +
1024
+ files.data.length +
1025
+ files.other.length;
1026
+ if (totalFiles > 0) {
1027
+ spinner.start(`Installing ${module} dependencies...`);
1028
+ await this.installPartialModule(module, bmadDir, files);
1029
+ spinner.succeed(`${module} dependencies installed`);
1030
+ }
1031
+ }
1032
+ }
1033
+ }
1034
+
1035
+ // All content is now installed as modules - no separate custom content handling needed
1036
+
1037
+ // Generate clean config.yaml files for each installed module
1038
+ spinner.start('Generating module configurations...');
1039
+ await this.generateModuleConfigs(bmadDir, moduleConfigs);
1040
+ spinner.succeed('Module configurations generated');
1041
+
1042
+ // Create agent configuration files
1043
+ // Note: Legacy createAgentConfigs removed - using YAML customize system instead
1044
+ // Customize templates are now created in processAgentFiles when building YAML agents
1045
+
1046
+ // Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion)
1047
+ const cfgDir = path.join(bmadDir, '_config');
1048
+ this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
1049
+ this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
1050
+ this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
1051
+ this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
1052
+
1053
+ // Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup
1054
+ spinner.start('Generating workflow and agent manifests...');
1055
+ const manifestGen = new ManifestGenerator();
1056
+
1057
+ // For quick update, we need ALL installed modules in the manifest
1058
+ // Not just the ones being updated
1059
+ const allModulesForManifest = config._quickUpdate
1060
+ ? config._existingModules || allModules || []
1061
+ : config._preserveModules
1062
+ ? [...allModules, ...config._preserveModules]
1063
+ : allModules || [];
1064
+
1065
+ // For regular installs (including when called from quick update), use what we have
1066
+ let modulesForCsvPreserve;
1067
+ if (config._quickUpdate) {
1068
+ // Quick update - use existing modules or fall back to modules being updated
1069
+ modulesForCsvPreserve = config._existingModules || allModules || [];
1070
+ } else {
1071
+ // Regular install - use the modules we're installing plus any preserved ones
1072
+ modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
1073
+ }
1074
+
1075
+ const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
1076
+ ides: config.ides || [],
1077
+ preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
1078
+ });
1079
+
1080
+ // Custom modules are now included in the main modules list - no separate tracking needed
1081
+
1082
+ spinner.succeed(
1083
+ `Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
1084
+ );
1085
+
1086
+ // Configure IDEs and copy documentation
1087
+ if (!config.skipIde && config.ides && config.ides.length > 0) {
1088
+ // Filter out any undefined/null values from the IDE list
1089
+ const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
1090
+
1091
+ if (validIdes.length === 0) {
1092
+ console.log(chalk.yellow('⚠️ No valid IDEs selected. Skipping IDE configuration.'));
1093
+ } else {
1094
+ // Check if any IDE might need prompting (no pre-collected config)
1095
+ const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
1096
+
1097
+ if (!needsPrompting) {
1098
+ spinner.start('Configuring IDEs...');
1099
+ }
1100
+
1101
+ // Temporarily suppress console output if not verbose
1102
+ const originalLog = console.log;
1103
+ if (!config.verbose) {
1104
+ console.log = () => { };
1105
+ }
1106
+
1107
+ for (const ide of validIdes) {
1108
+ // Only show spinner if we have pre-collected config (no prompts expected)
1109
+ if (ideConfigurations[ide] && !needsPrompting) {
1110
+ spinner.text = `Configuring ${ide}...`;
1111
+ } else if (!ideConfigurations[ide]) {
1112
+ // Stop spinner before prompting
1113
+ if (spinner.isSpinning) {
1114
+ spinner.stop();
1115
+ }
1116
+ console.log(chalk.cyan(`\nConfiguring ${ide}...`));
1117
+ }
1118
+
1119
+ // Pass pre-collected configuration to avoid re-prompting
1120
+ await this.ideManager.setup(ide, projectDir, bmadDir, {
1121
+ selectedModules: allModules || [],
1122
+ preCollectedConfig: ideConfigurations[ide] || null,
1123
+ verbose: config.verbose,
1124
+ });
1125
+
1126
+ // Save IDE configuration for future updates
1127
+ if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
1128
+ await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
1129
+ }
1130
+
1131
+ // Restart spinner if we stopped it
1132
+ if (!ideConfigurations[ide] && !spinner.isSpinning) {
1133
+ spinner.start('Configuring IDEs...');
1134
+ }
1135
+ }
1136
+
1137
+ // Restore console.log
1138
+ console.log = originalLog;
1139
+
1140
+ if (spinner.isSpinning) {
1141
+ spinner.succeed(`Configured: ${validIdes.join(', ')}`);
1142
+ } else {
1143
+ console.log(chalk.green(`✓ Configured: ${validIdes.join(', ')}`));
1144
+ }
1145
+ }
1146
+ }
1147
+
1148
+ // Run module-specific installers after IDE setup
1149
+ spinner.start('Running module-specific installers...');
1150
+
1151
+ // Create a conditional logger based on verbose mode
1152
+ const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
1153
+ const moduleLogger = {
1154
+ log: (msg) => (verboseMode ? console.log(msg) : {}), // Only log in verbose mode
1155
+ error: (msg) => console.error(msg), // Always show errors
1156
+ warn: (msg) => console.warn(msg), // Always show warnings
1157
+ };
1158
+
1159
+ // Run core module installer if core was installed
1160
+ if (config.installCore || resolution.byModule.core) {
1161
+ spinner.text = 'Running core module installer...';
1162
+
1163
+ await this.moduleManager.runModuleInstaller('core', bmadDir, {
1164
+ installedIDEs: config.ides || [],
1165
+ moduleConfig: moduleConfigs.core || {},
1166
+ coreConfig: moduleConfigs.core || {},
1167
+ logger: moduleLogger,
1168
+ });
1169
+ }
1170
+
1171
+ // Run installers for user-selected modules
1172
+ if (config.modules && config.modules.length > 0) {
1173
+ for (const moduleName of config.modules) {
1174
+ spinner.text = `Running ${moduleName} module installer...`;
1175
+
1176
+ // Pass installed IDEs and module config to module installer
1177
+ await this.moduleManager.runModuleInstaller(moduleName, bmadDir, {
1178
+ installedIDEs: config.ides || [],
1179
+ moduleConfig: moduleConfigs[moduleName] || {},
1180
+ coreConfig: moduleConfigs.core || {},
1181
+ logger: moduleLogger,
1182
+ });
1183
+ }
1184
+ }
1185
+
1186
+ spinner.succeed('Module-specific installers completed');
1187
+
1188
+ // Note: Manifest files are already created by ManifestGenerator above
1189
+ // No need to create legacy manifest.csv anymore
1190
+
1191
+ // If this was an update, restore custom files
1192
+ let customFiles = [];
1193
+ let modifiedFiles = [];
1194
+ if (config._isUpdate) {
1195
+ if (config._customFiles && config._customFiles.length > 0) {
1196
+ spinner.start(`Restoring ${config._customFiles.length} custom files...`);
1197
+
1198
+ for (const originalPath of config._customFiles) {
1199
+ const relativePath = path.relative(bmadDir, originalPath);
1200
+ const backupPath = path.join(config._tempBackupDir, relativePath);
1201
+
1202
+ if (await fs.pathExists(backupPath)) {
1203
+ await fs.ensureDir(path.dirname(originalPath));
1204
+ await fs.copy(backupPath, originalPath, { overwrite: true });
1205
+ }
1206
+ }
1207
+
1208
+ // Clean up temp backup
1209
+ if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
1210
+ await fs.remove(config._tempBackupDir);
1211
+ }
1212
+
1213
+ spinner.succeed(`Restored ${config._customFiles.length} custom files`);
1214
+ customFiles = config._customFiles;
1215
+ }
1216
+
1217
+ if (config._modifiedFiles && config._modifiedFiles.length > 0) {
1218
+ modifiedFiles = config._modifiedFiles;
1219
+
1220
+ // Restore modified files as .bak files
1221
+ if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
1222
+ spinner.start(`Restoring ${modifiedFiles.length} modified files as .bak...`);
1223
+
1224
+ for (const modifiedFile of modifiedFiles) {
1225
+ const relativePath = path.relative(bmadDir, modifiedFile.path);
1226
+ const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath);
1227
+ const bakPath = modifiedFile.path + '.bak';
1228
+
1229
+ if (await fs.pathExists(tempBackupPath)) {
1230
+ await fs.ensureDir(path.dirname(bakPath));
1231
+ await fs.copy(tempBackupPath, bakPath, { overwrite: true });
1232
+ }
1233
+ }
1234
+
1235
+ // Clean up temp backup
1236
+ await fs.remove(config._tempModifiedBackupDir);
1237
+
1238
+ spinner.succeed(`Restored ${modifiedFiles.length} modified files as .bak`);
1239
+ }
1240
+ }
1241
+ }
1242
+
1243
+ spinner.stop();
1244
+
1245
+ // Report custom and modified files if any were found
1246
+ if (customFiles.length > 0) {
1247
+ console.log(chalk.cyan(`\n📁 Custom files preserved: ${customFiles.length}`));
1248
+ }
1249
+
1250
+ if (modifiedFiles.length > 0) {
1251
+ console.log(chalk.yellow(`\n⚠️ User modified files detected: ${modifiedFiles.length}`));
1252
+ console.log(
1253
+ chalk.dim(
1254
+ '\nThese user modified files have been updated with the new version, search the project for .bak files that had your customizations.',
1255
+ ),
1256
+ );
1257
+ console.log(chalk.dim('Remove these .bak files it no longer needed\n'));
1258
+ }
1259
+
1260
+ // Display completion message
1261
+ const { UI } = require('../../../lib/ui');
1262
+ const ui = new UI();
1263
+ ui.showInstallSummary({
1264
+ path: bmadDir,
1265
+ modules: config.modules,
1266
+ ides: config.ides,
1267
+ customFiles: customFiles.length > 0 ? customFiles : undefined,
1268
+ ttsInjectedFiles: this.enableAgentVibes && this.ttsInjectedFiles.length > 0 ? this.ttsInjectedFiles : undefined,
1269
+ agentVibesEnabled: this.enableAgentVibes || false,
1270
+ });
1271
+
1272
+ return {
1273
+ success: true,
1274
+ path: bmadDir,
1275
+ modules: config.modules,
1276
+ ides: config.ides,
1277
+ needsAgentVibes: this.enableAgentVibes && !config.agentVibesInstalled,
1278
+ projectDir: projectDir,
1279
+ };
1280
+ } catch (error) {
1281
+ spinner.fail('Installation failed');
1282
+ throw error;
1283
+ }
1284
+ }
1285
+
1286
+ /**
1287
+ * Update existing installation
1288
+ */
1289
+ async update(config) {
1290
+ const spinner = ora('Checking installation...').start();
1291
+
1292
+ try {
1293
+ const projectDir = path.resolve(config.directory);
1294
+ const { bmadDir } = await this.findBmadDir(projectDir);
1295
+ const existingInstall = await this.detector.detect(bmadDir);
1296
+
1297
+ if (!existingInstall.installed) {
1298
+ spinner.fail('No BMAD installation found');
1299
+ throw new Error(`No BMAD installation found at ${bmadDir}`);
1300
+ }
1301
+
1302
+ spinner.text = 'Analyzing update requirements...';
1303
+
1304
+ // Compare versions and determine what needs updating
1305
+ const currentVersion = existingInstall.version;
1306
+ const newVersion = require(path.join(getProjectRoot(), 'package.json')).version;
1307
+
1308
+ // Check for custom modules with missing sources before update
1309
+ const customModuleSources = new Map();
1310
+
1311
+ // Check manifest for backward compatibility
1312
+ if (existingInstall.customModules) {
1313
+ for (const customModule of existingInstall.customModules) {
1314
+ customModuleSources.set(customModule.id, customModule);
1315
+ }
1316
+ }
1317
+
1318
+ // Also check cache directory
1319
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
1320
+ if (await fs.pathExists(cacheDir)) {
1321
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
1322
+
1323
+ for (const cachedModule of cachedModules) {
1324
+ if (cachedModule.isDirectory()) {
1325
+ const moduleId = cachedModule.name;
1326
+
1327
+ // Skip if we already have this module
1328
+ if (customModuleSources.has(moduleId)) {
1329
+ continue;
1330
+ }
1331
+
1332
+ const cachedPath = path.join(cacheDir, moduleId);
1333
+
1334
+ // Check if this is actually a custom module (has module.yaml)
1335
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
1336
+ if (await fs.pathExists(moduleYamlPath)) {
1337
+ customModuleSources.set(moduleId, {
1338
+ id: moduleId,
1339
+ name: moduleId,
1340
+ sourcePath: path.join('_config', 'custom', moduleId), // Relative path
1341
+ cached: true,
1342
+ });
1343
+ }
1344
+ }
1345
+ }
1346
+ }
1347
+
1348
+ if (customModuleSources.size > 0) {
1349
+ spinner.stop();
1350
+ console.log(chalk.yellow('\nChecking custom module sources before update...'));
1351
+
1352
+ const projectRoot = getProjectRoot();
1353
+ await this.handleMissingCustomSources(
1354
+ customModuleSources,
1355
+ bmadDir,
1356
+ projectRoot,
1357
+ 'update',
1358
+ existingInstall.modules.map((m) => m.id),
1359
+ );
1360
+
1361
+ spinner.start('Preparing update...');
1362
+ }
1363
+
1364
+ if (config.dryRun) {
1365
+ spinner.stop();
1366
+ console.log(chalk.cyan('\n🔍 Update Preview (Dry Run)\n'));
1367
+ console.log(chalk.bold('Current version:'), currentVersion);
1368
+ console.log(chalk.bold('New version:'), newVersion);
1369
+ console.log(chalk.bold('Core:'), existingInstall.hasCore ? 'Will be updated' : 'Not installed');
1370
+
1371
+ if (existingInstall.modules.length > 0) {
1372
+ console.log(chalk.bold('\nModules to update:'));
1373
+ for (const mod of existingInstall.modules) {
1374
+ console.log(` - ${mod.id}`);
1375
+ }
1376
+ }
1377
+ return;
1378
+ }
1379
+
1380
+ // Perform actual update
1381
+ if (existingInstall.hasCore) {
1382
+ spinner.text = 'Updating core...';
1383
+ await this.updateCore(bmadDir, config.force);
1384
+ }
1385
+
1386
+ for (const module of existingInstall.modules) {
1387
+ spinner.text = `Updating module: ${module.id}...`;
1388
+ await this.moduleManager.update(module.id, bmadDir, config.force);
1389
+ }
1390
+
1391
+ // Update manifest
1392
+ spinner.text = 'Updating manifest...';
1393
+ await this.manifest.update(bmadDir, {
1394
+ version: newVersion,
1395
+ updateDate: new Date().toISOString(),
1396
+ });
1397
+
1398
+ spinner.succeed('Update complete');
1399
+ return { success: true };
1400
+ } catch (error) {
1401
+ spinner.fail('Update failed');
1402
+ throw error;
1403
+ }
1404
+ }
1405
+
1406
+ /**
1407
+ * Get installation status
1408
+ */
1409
+ async getStatus(directory) {
1410
+ const projectDir = path.resolve(directory);
1411
+ const { bmadDir } = await this.findBmadDir(projectDir);
1412
+ return await this.detector.detect(bmadDir);
1413
+ }
1414
+
1415
+ /**
1416
+ * Get available modules
1417
+ */
1418
+ async getAvailableModules() {
1419
+ return await this.moduleManager.listAvailable();
1420
+ }
1421
+
1422
+ /**
1423
+ * Uninstall BMAD
1424
+ */
1425
+ async uninstall(directory) {
1426
+ const projectDir = path.resolve(directory);
1427
+ const { bmadDir } = await this.findBmadDir(projectDir);
1428
+
1429
+ if (await fs.pathExists(bmadDir)) {
1430
+ await fs.remove(bmadDir);
1431
+ }
1432
+
1433
+ // Clean up IDE configurations
1434
+ await this.ideManager.cleanup(projectDir);
1435
+
1436
+ return { success: true };
1437
+ }
1438
+
1439
+ /**
1440
+ * Private: Create directory structure
1441
+ */
1442
+ async createDirectoryStructure(bmadDir) {
1443
+ await fs.ensureDir(bmadDir);
1444
+ await fs.ensureDir(path.join(bmadDir, '_config'));
1445
+ await fs.ensureDir(path.join(bmadDir, '_config', 'agents'));
1446
+ await fs.ensureDir(path.join(bmadDir, '_config', 'custom'));
1447
+ }
1448
+
1449
+ /**
1450
+ * Generate clean config.yaml files for each installed module
1451
+ * @param {string} bmadDir - BMAD installation directory
1452
+ * @param {Object} moduleConfigs - Collected configuration values
1453
+ */
1454
+ async generateModuleConfigs(bmadDir, moduleConfigs) {
1455
+ const yaml = require('yaml');
1456
+
1457
+ // Extract core config values to share with other modules
1458
+ const coreConfig = moduleConfigs.core || {};
1459
+
1460
+ // Get all installed module directories
1461
+ const entries = await fs.readdir(bmadDir, { withFileTypes: true });
1462
+ const installedModules = entries
1463
+ .filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs')
1464
+ .map((entry) => entry.name);
1465
+
1466
+ // Generate config.yaml for each installed module
1467
+ for (const moduleName of installedModules) {
1468
+ const modulePath = path.join(bmadDir, moduleName);
1469
+
1470
+ // Get module-specific config or use empty object if none
1471
+ const config = moduleConfigs[moduleName] || {};
1472
+
1473
+ if (await fs.pathExists(modulePath)) {
1474
+ const configPath = path.join(modulePath, 'config.yaml');
1475
+
1476
+ // Create header
1477
+ const packageJson = require(path.join(getProjectRoot(), 'package.json'));
1478
+ const header = `# ${moduleName.toUpperCase()} Module Configuration
1479
+ # Generated by BMAD installer
1480
+ # Version: ${packageJson.version}
1481
+ # Date: ${new Date().toISOString()}
1482
+
1483
+ `;
1484
+
1485
+ // For non-core modules, add core config values directly
1486
+ let finalConfig = { ...config };
1487
+ let coreSection = '';
1488
+
1489
+ if (moduleName !== 'core' && coreConfig && Object.keys(coreConfig).length > 0) {
1490
+ // Add core values directly to the module config
1491
+ // These will be available for reference in the module
1492
+ finalConfig = {
1493
+ ...config,
1494
+ ...coreConfig, // Spread core config values directly into the module config
1495
+ };
1496
+
1497
+ // Create a comment section to identify core values
1498
+ coreSection = '\n# Core Configuration Values\n';
1499
+ }
1500
+
1501
+ // Clean the config to remove any non-serializable values (like functions)
1502
+ const cleanConfig = structuredClone(finalConfig);
1503
+
1504
+ // Convert config to YAML
1505
+ let yamlContent = yaml.stringify(cleanConfig, {
1506
+ indent: 2,
1507
+ lineWidth: 0,
1508
+ minContentWidth: 0,
1509
+ });
1510
+
1511
+ // If we have core values, reorganize the YAML to group them with their comment
1512
+ if (coreSection && moduleName !== 'core') {
1513
+ // Split the YAML into lines
1514
+ const lines = yamlContent.split('\n');
1515
+ const moduleConfigLines = [];
1516
+ const coreConfigLines = [];
1517
+
1518
+ // Separate module-specific and core config lines
1519
+ for (const line of lines) {
1520
+ const key = line.split(':')[0].trim();
1521
+ if (Object.prototype.hasOwnProperty.call(coreConfig, key)) {
1522
+ coreConfigLines.push(line);
1523
+ } else {
1524
+ moduleConfigLines.push(line);
1525
+ }
1526
+ }
1527
+
1528
+ // Rebuild YAML with module config first, then core config with comment
1529
+ yamlContent = moduleConfigLines.join('\n');
1530
+ if (coreConfigLines.length > 0) {
1531
+ yamlContent += coreSection + coreConfigLines.join('\n');
1532
+ }
1533
+ }
1534
+
1535
+ // Write the clean config file with POSIX-compliant final newline
1536
+ const content = header + yamlContent;
1537
+ await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8');
1538
+
1539
+ // Track the config file in installedFiles
1540
+ this.installedFiles.add(configPath);
1541
+ }
1542
+ }
1543
+ }
1544
+
1545
+ /**
1546
+ * Install core with resolved dependencies
1547
+ * @param {string} bmadDir - BMAD installation directory
1548
+ * @param {Object} coreFiles - Core files to install
1549
+ */
1550
+ async installCoreWithDependencies(bmadDir, coreFiles) {
1551
+ const sourcePath = getModulePath('core');
1552
+ const targetPath = path.join(bmadDir, 'core');
1553
+ await this.installCore(bmadDir);
1554
+ }
1555
+
1556
+ /**
1557
+ * Install module with resolved dependencies
1558
+ * @param {string} moduleName - Module name
1559
+ * @param {string} bmadDir - BMAD installation directory
1560
+ * @param {Object} moduleFiles - Module files to install
1561
+ */
1562
+ async installModuleWithDependencies(moduleName, bmadDir, moduleFiles) {
1563
+ // Get module configuration for conditional installation
1564
+ const moduleConfig = this.configCollector.collectedConfig[moduleName] || {};
1565
+
1566
+ // Use existing module manager for full installation with file tracking
1567
+ // Note: Module-specific installers are called separately after IDE setup
1568
+ await this.moduleManager.install(
1569
+ moduleName,
1570
+ bmadDir,
1571
+ (filePath) => {
1572
+ this.installedFiles.add(filePath);
1573
+ },
1574
+ {
1575
+ skipModuleInstaller: true, // We'll run it later after IDE setup
1576
+ moduleConfig: moduleConfig, // Pass module config for conditional filtering
1577
+ installer: this,
1578
+ },
1579
+ );
1580
+
1581
+ // Process agent files to build YAML agents and create customize templates
1582
+ const modulePath = path.join(bmadDir, moduleName);
1583
+ await this.processAgentFiles(modulePath, moduleName);
1584
+
1585
+ // Dependencies are already included in full module install
1586
+ }
1587
+
1588
+ /**
1589
+ * Install partial module (only dependencies needed by other modules)
1590
+ */
1591
+ async installPartialModule(moduleName, bmadDir, files) {
1592
+ const sourceBase = getModulePath(moduleName);
1593
+ const targetBase = path.join(bmadDir, moduleName);
1594
+
1595
+ // Create module directory
1596
+ await fs.ensureDir(targetBase);
1597
+
1598
+ // Copy only the required dependency files
1599
+ if (files.agents && files.agents.length > 0) {
1600
+ const agentsDir = path.join(targetBase, 'agents');
1601
+ await fs.ensureDir(agentsDir);
1602
+
1603
+ for (const agentPath of files.agents) {
1604
+ const fileName = path.basename(agentPath);
1605
+ const sourcePath = path.join(sourceBase, 'agents', fileName);
1606
+ const targetPath = path.join(agentsDir, fileName);
1607
+
1608
+ if (await fs.pathExists(sourcePath)) {
1609
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
1610
+ this.installedFiles.add(targetPath);
1611
+ }
1612
+ }
1613
+ }
1614
+
1615
+ if (files.tasks && files.tasks.length > 0) {
1616
+ const tasksDir = path.join(targetBase, 'tasks');
1617
+ await fs.ensureDir(tasksDir);
1618
+
1619
+ for (const taskPath of files.tasks) {
1620
+ const fileName = path.basename(taskPath);
1621
+ const sourcePath = path.join(sourceBase, 'tasks', fileName);
1622
+ const targetPath = path.join(tasksDir, fileName);
1623
+
1624
+ if (await fs.pathExists(sourcePath)) {
1625
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
1626
+ this.installedFiles.add(targetPath);
1627
+ }
1628
+ }
1629
+ }
1630
+
1631
+ if (files.tools && files.tools.length > 0) {
1632
+ const toolsDir = path.join(targetBase, 'tools');
1633
+ await fs.ensureDir(toolsDir);
1634
+
1635
+ for (const toolPath of files.tools) {
1636
+ const fileName = path.basename(toolPath);
1637
+ const sourcePath = path.join(sourceBase, 'tools', fileName);
1638
+ const targetPath = path.join(toolsDir, fileName);
1639
+
1640
+ if (await fs.pathExists(sourcePath)) {
1641
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
1642
+ this.installedFiles.add(targetPath);
1643
+ }
1644
+ }
1645
+ }
1646
+
1647
+ if (files.templates && files.templates.length > 0) {
1648
+ const templatesDir = path.join(targetBase, 'templates');
1649
+ await fs.ensureDir(templatesDir);
1650
+
1651
+ for (const templatePath of files.templates) {
1652
+ const fileName = path.basename(templatePath);
1653
+ const sourcePath = path.join(sourceBase, 'templates', fileName);
1654
+ const targetPath = path.join(templatesDir, fileName);
1655
+
1656
+ if (await fs.pathExists(sourcePath)) {
1657
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
1658
+ this.installedFiles.add(targetPath);
1659
+ }
1660
+ }
1661
+ }
1662
+
1663
+ if (files.data && files.data.length > 0) {
1664
+ for (const dataPath of files.data) {
1665
+ // Preserve directory structure for data files
1666
+ const relative = path.relative(sourceBase, dataPath);
1667
+ const targetPath = path.join(targetBase, relative);
1668
+
1669
+ await fs.ensureDir(path.dirname(targetPath));
1670
+
1671
+ if (await fs.pathExists(dataPath)) {
1672
+ await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
1673
+ this.installedFiles.add(targetPath);
1674
+ }
1675
+ }
1676
+ }
1677
+
1678
+ // Create a marker file to indicate this is a partial installation
1679
+ const markerPath = path.join(targetBase, '.partial');
1680
+ await fs.writeFile(
1681
+ markerPath,
1682
+ `This module contains only dependencies required by other modules.\nInstalled: ${new Date().toISOString()}\n`,
1683
+ );
1684
+ }
1685
+
1686
+ /**
1687
+ * Private: Install core
1688
+ * @param {string} bmadDir - BMAD installation directory
1689
+ */
1690
+ async installCore(bmadDir) {
1691
+ const sourcePath = getModulePath('core');
1692
+ const targetPath = path.join(bmadDir, 'core');
1693
+
1694
+ // Copy core files (skip .agent.yaml files like modules do)
1695
+ await this.copyCoreFiles(sourcePath, targetPath);
1696
+
1697
+ // Compile agents using the same compiler as modules
1698
+ const { ModuleManager } = require('../modules/manager');
1699
+ const moduleManager = new ModuleManager();
1700
+ await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir, this);
1701
+
1702
+ // Process agent files to inject activation block
1703
+ await this.processAgentFiles(targetPath, 'core');
1704
+ }
1705
+
1706
+ /**
1707
+ * Copy core files (similar to copyModuleWithFiltering but for core)
1708
+ * @param {string} sourcePath - Source path
1709
+ * @param {string} targetPath - Target path
1710
+ */
1711
+ async copyCoreFiles(sourcePath, targetPath) {
1712
+ // Get all files in source
1713
+ const files = await this.getFileList(sourcePath);
1714
+
1715
+ for (const file of files) {
1716
+ // Skip sub-modules directory - these are IDE-specific and handled separately
1717
+ if (file.startsWith('sub-modules/')) {
1718
+ continue;
1719
+ }
1720
+
1721
+ // Skip sidecar directories - they are handled separately during agent compilation
1722
+ if (
1723
+ path
1724
+ .dirname(file)
1725
+ .split('/')
1726
+ .some((dir) => dir.toLowerCase().includes('sidecar'))
1727
+ ) {
1728
+ continue;
1729
+ }
1730
+
1731
+ // Skip _module-installer directory - it's only needed at install time
1732
+ if (file.startsWith('_module-installer/') || file === 'module.yaml') {
1733
+ continue;
1734
+ }
1735
+
1736
+ // Skip config.yaml templates - we'll generate clean ones with actual values
1737
+ if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) {
1738
+ continue;
1739
+ }
1740
+
1741
+ // Skip .agent.yaml files - they will be compiled separately
1742
+ if (file.endsWith('.agent.yaml')) {
1743
+ continue;
1744
+ }
1745
+
1746
+ const sourceFile = path.join(sourcePath, file);
1747
+ const targetFile = path.join(targetPath, file);
1748
+
1749
+ // Check if this is an agent file
1750
+ if (file.startsWith('agents/') && file.endsWith('.md')) {
1751
+ // Read the file to check for localskip
1752
+ const content = await fs.readFile(sourceFile, 'utf8');
1753
+
1754
+ // Check for localskip="true" in the agent tag
1755
+ const agentMatch = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
1756
+ if (agentMatch) {
1757
+ console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`));
1758
+ continue; // Skip this agent
1759
+ }
1760
+ }
1761
+
1762
+ // Check if this is a workflow.yaml file
1763
+ if (file.endsWith('workflow.yaml')) {
1764
+ await fs.ensureDir(path.dirname(targetFile));
1765
+ await this.copyWorkflowYamlStripped(sourceFile, targetFile);
1766
+ } else {
1767
+ // Copy the file with placeholder replacement
1768
+ await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad');
1769
+ }
1770
+
1771
+ // Track the installed file
1772
+ this.installedFiles.add(targetFile);
1773
+ }
1774
+ }
1775
+
1776
+ /**
1777
+ * Get list of all files in a directory recursively
1778
+ * @param {string} dir - Directory path
1779
+ * @param {string} baseDir - Base directory for relative paths
1780
+ * @returns {Array} List of relative file paths
1781
+ */
1782
+ async getFileList(dir, baseDir = dir) {
1783
+ const files = [];
1784
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1785
+
1786
+ for (const entry of entries) {
1787
+ const fullPath = path.join(dir, entry.name);
1788
+
1789
+ if (entry.isDirectory()) {
1790
+ // Skip _module-installer directories
1791
+ if (entry.name === '_module-installer') {
1792
+ continue;
1793
+ }
1794
+ const subFiles = await this.getFileList(fullPath, baseDir);
1795
+ files.push(...subFiles);
1796
+ } else {
1797
+ files.push(path.relative(baseDir, fullPath));
1798
+ }
1799
+ }
1800
+
1801
+ return files;
1802
+ }
1803
+
1804
+ /**
1805
+ * Process agent files to build YAML agents and inject activation blocks
1806
+ * @param {string} modulePath - Path to module in bmad/ installation
1807
+ * @param {string} moduleName - Module name
1808
+ */
1809
+ async processAgentFiles(modulePath, moduleName) {
1810
+ const agentsPath = path.join(modulePath, 'agents');
1811
+
1812
+ // Check if agents directory exists
1813
+ if (!(await fs.pathExists(agentsPath))) {
1814
+ return; // No agents to process
1815
+ }
1816
+
1817
+ // Determine project directory (parent of bmad/ directory)
1818
+ const bmadDir = path.dirname(modulePath);
1819
+ const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
1820
+
1821
+ // Ensure _config/agents directory exists
1822
+ await fs.ensureDir(cfgAgentsDir);
1823
+
1824
+ // Get all agent files
1825
+ const agentFiles = await fs.readdir(agentsPath);
1826
+
1827
+ for (const agentFile of agentFiles) {
1828
+ // Skip .agent.yaml files - they should already be compiled by compileModuleAgents
1829
+ if (agentFile.endsWith('.agent.yaml')) {
1830
+ continue;
1831
+ }
1832
+
1833
+ // Only process .md files (already compiled from YAML)
1834
+ if (!agentFile.endsWith('.md')) {
1835
+ continue;
1836
+ }
1837
+
1838
+ const agentName = agentFile.replace('.md', '');
1839
+ const mdPath = path.join(agentsPath, agentFile);
1840
+ const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
1841
+
1842
+ // For .md files that are already compiled, we don't need to do much
1843
+ // Just ensure the customize template exists
1844
+ if (!(await fs.pathExists(customizePath))) {
1845
+ const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
1846
+ if (await fs.pathExists(genericTemplatePath)) {
1847
+ await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad');
1848
+ if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
1849
+ console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
1850
+ }
1851
+ }
1852
+ }
1853
+ }
1854
+ }
1855
+
1856
+ /**
1857
+ * Build standalone agents in bmad/agents/ directory
1858
+ * @param {string} bmadDir - Path to bmad directory
1859
+ * @param {string} projectDir - Path to project directory
1860
+ */
1861
+ async buildStandaloneAgents(bmadDir, projectDir) {
1862
+ const standaloneAgentsPath = path.join(bmadDir, 'agents');
1863
+ const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
1864
+
1865
+ // Check if standalone agents directory exists
1866
+ if (!(await fs.pathExists(standaloneAgentsPath))) {
1867
+ return;
1868
+ }
1869
+
1870
+ // Get all subdirectories in agents/
1871
+ const agentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
1872
+
1873
+ for (const agentDir of agentDirs) {
1874
+ if (!agentDir.isDirectory()) continue;
1875
+
1876
+ const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
1877
+
1878
+ // Find any .agent.yaml file in the directory
1879
+ const files = await fs.readdir(agentDirPath);
1880
+ const yamlFile = files.find((f) => f.endsWith('.agent.yaml'));
1881
+
1882
+ if (!yamlFile) continue;
1883
+
1884
+ const agentName = path.basename(yamlFile, '.agent.yaml');
1885
+ const sourceYamlPath = path.join(agentDirPath, yamlFile);
1886
+ const targetMdPath = path.join(agentDirPath, `${agentName}.md`);
1887
+ const customizePath = path.join(cfgAgentsDir, `${agentName}.customize.yaml`);
1888
+
1889
+ // Check for customizations
1890
+ const customizeExists = await fs.pathExists(customizePath);
1891
+ let customizedFields = [];
1892
+
1893
+ if (customizeExists) {
1894
+ const customizeContent = await fs.readFile(customizePath, 'utf8');
1895
+ const yaml = require('yaml');
1896
+ const customizeYaml = yaml.parse(customizeContent);
1897
+
1898
+ // Detect what fields are customized (similar to rebuildAgentFiles)
1899
+ if (customizeYaml) {
1900
+ if (customizeYaml.persona) {
1901
+ for (const [key, value] of Object.entries(customizeYaml.persona)) {
1902
+ if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
1903
+ customizedFields.push(`persona.${key}`);
1904
+ }
1905
+ }
1906
+ }
1907
+ if (customizeYaml.agent?.metadata) {
1908
+ for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
1909
+ if (value !== '' && value !== null) {
1910
+ customizedFields.push(`metadata.${key}`);
1911
+ }
1912
+ }
1913
+ }
1914
+ if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
1915
+ customizedFields.push('critical_actions');
1916
+ }
1917
+ if (customizeYaml.menu && customizeYaml.menu.length > 0) {
1918
+ customizedFields.push('menu');
1919
+ }
1920
+ }
1921
+ }
1922
+
1923
+ // Build YAML to XML .md
1924
+ let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
1925
+ includeMetadata: true,
1926
+ });
1927
+
1928
+ // DO NOT replace {project-root} - LLMs understand this placeholder at runtime
1929
+ // const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
1930
+
1931
+ // Process TTS injection points (pass targetPath for tracking)
1932
+ xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
1933
+
1934
+ // Write the built .md file with POSIX-compliant final newline
1935
+ const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
1936
+ await fs.writeFile(targetMdPath, content, 'utf8');
1937
+
1938
+ // Display result
1939
+ if (customizedFields.length > 0) {
1940
+ console.log(chalk.dim(` Built standalone agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
1941
+ } else {
1942
+ console.log(chalk.dim(` Built standalone agent: ${agentName}.md`));
1943
+ }
1944
+ }
1945
+ }
1946
+
1947
+ /**
1948
+ * Rebuild agent files from installer source (for compile command)
1949
+ * @param {string} modulePath - Path to module in bmad/ installation
1950
+ * @param {string} moduleName - Module name
1951
+ */
1952
+ async rebuildAgentFiles(modulePath, moduleName) {
1953
+ // Get source agents directory from installer
1954
+ const sourceAgentsPath =
1955
+ moduleName === 'core' ? path.join(getModulePath('core'), 'agents') : path.join(getSourcePath(`modules/${moduleName}`), 'agents');
1956
+
1957
+ if (!(await fs.pathExists(sourceAgentsPath))) {
1958
+ return; // No source agents to rebuild
1959
+ }
1960
+
1961
+ // Determine project directory (parent of bmad/ directory)
1962
+ const bmadDir = path.dirname(modulePath);
1963
+ const projectDir = path.dirname(bmadDir);
1964
+ const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
1965
+ const targetAgentsPath = path.join(modulePath, 'agents');
1966
+
1967
+ // Ensure target directory exists
1968
+ await fs.ensureDir(targetAgentsPath);
1969
+
1970
+ // Get all YAML agent files from source
1971
+ const sourceFiles = await fs.readdir(sourceAgentsPath);
1972
+
1973
+ for (const file of sourceFiles) {
1974
+ if (file.endsWith('.agent.yaml')) {
1975
+ const agentName = file.replace('.agent.yaml', '');
1976
+ const sourceYamlPath = path.join(sourceAgentsPath, file);
1977
+ const targetMdPath = path.join(targetAgentsPath, `${agentName}.md`);
1978
+ const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
1979
+
1980
+ // Check for customizations
1981
+ const customizeExists = await fs.pathExists(customizePath);
1982
+ let customizedFields = [];
1983
+
1984
+ if (customizeExists) {
1985
+ const customizeContent = await fs.readFile(customizePath, 'utf8');
1986
+ const yaml = require('yaml');
1987
+ const customizeYaml = yaml.parse(customizeContent);
1988
+
1989
+ // Detect what fields are customized
1990
+ if (customizeYaml) {
1991
+ if (customizeYaml.persona) {
1992
+ for (const [key, value] of Object.entries(customizeYaml.persona)) {
1993
+ if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
1994
+ customizedFields.push(`persona.${key}`);
1995
+ }
1996
+ }
1997
+ }
1998
+ if (customizeYaml.agent?.metadata) {
1999
+ for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
2000
+ if (value !== '' && value !== null) {
2001
+ customizedFields.push(`metadata.${key}`);
2002
+ }
2003
+ }
2004
+ }
2005
+ if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
2006
+ customizedFields.push('critical_actions');
2007
+ }
2008
+ if (customizeYaml.memories && customizeYaml.memories.length > 0) {
2009
+ customizedFields.push('memories');
2010
+ }
2011
+ if (customizeYaml.menu && customizeYaml.menu.length > 0) {
2012
+ customizedFields.push('menu');
2013
+ }
2014
+ if (customizeYaml.prompts && customizeYaml.prompts.length > 0) {
2015
+ customizedFields.push('prompts');
2016
+ }
2017
+ }
2018
+ }
2019
+
2020
+ // Read the YAML content
2021
+ const yamlContent = await fs.readFile(sourceYamlPath, 'utf8');
2022
+
2023
+ // Read customize content if exists
2024
+ let customizeData = {};
2025
+ if (customizeExists) {
2026
+ const customizeContent = await fs.readFile(customizePath, 'utf8');
2027
+ const yaml = require('yaml');
2028
+ customizeData = yaml.parse(customizeContent);
2029
+ }
2030
+
2031
+ // Build agent answers from customize data (filter empty values)
2032
+ const answers = {};
2033
+ if (customizeData.persona) {
2034
+ Object.assign(answers, filterCustomizationData(customizeData.persona));
2035
+ }
2036
+ if (customizeData.agent?.metadata) {
2037
+ const filteredMetadata = filterCustomizationData(customizeData.agent.metadata);
2038
+ if (Object.keys(filteredMetadata).length > 0) {
2039
+ Object.assign(answers, { metadata: filteredMetadata });
2040
+ }
2041
+ }
2042
+ if (customizeData.critical_actions && customizeData.critical_actions.length > 0) {
2043
+ answers.critical_actions = customizeData.critical_actions;
2044
+ }
2045
+ if (customizeData.memories && customizeData.memories.length > 0) {
2046
+ answers.memories = customizeData.memories;
2047
+ }
2048
+
2049
+ const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
2050
+ let coreConfig = {};
2051
+ if (await fs.pathExists(coreConfigPath)) {
2052
+ const yaml = require('yaml');
2053
+ const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
2054
+ coreConfig = yaml.parse(coreConfigContent);
2055
+ }
2056
+
2057
+ // Compile using the same compiler as initial installation
2058
+ const { compileAgent } = require('../../../lib/agent/compiler');
2059
+ const result = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
2060
+ config: coreConfig,
2061
+ });
2062
+
2063
+ // Check if compilation succeeded
2064
+ if (!result || !result.xml) {
2065
+ throw new Error(`Failed to compile agent ${agentName}: No XML returned from compiler`);
2066
+ }
2067
+
2068
+ // Replace _bmad with actual folder name if needed
2069
+ const finalXml = result.xml.replaceAll('_bmad', path.basename(bmadDir));
2070
+
2071
+ // Write the rebuilt .md file with POSIX-compliant final newline
2072
+ const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n';
2073
+ await fs.writeFile(targetMdPath, content, 'utf8');
2074
+
2075
+ // Display result with customizations if any
2076
+ if (customizedFields.length > 0) {
2077
+ console.log(chalk.dim(` Rebuilt agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
2078
+ } else {
2079
+ console.log(chalk.dim(` Rebuilt agent: ${agentName}.md`));
2080
+ }
2081
+ }
2082
+ }
2083
+ }
2084
+
2085
+ /**
2086
+ * Compile/rebuild all agents and tasks for quick updates
2087
+ * @param {Object} config - Compilation configuration
2088
+ * @returns {Object} Compilation results
2089
+ */
2090
+ async compileAgents(config) {
2091
+ try {
2092
+ const projectDir = path.resolve(config.directory);
2093
+ const { bmadDir } = await this.findBmadDir(projectDir);
2094
+
2095
+ // Check if bmad directory exists
2096
+ if (!(await fs.pathExists(bmadDir))) {
2097
+ throw new Error(`BMAD not installed at ${bmadDir}`);
2098
+ }
2099
+
2100
+ // Get installed modules from manifest
2101
+ const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
2102
+ let installedModules = [];
2103
+ let manifest = null;
2104
+ if (await fs.pathExists(manifestPath)) {
2105
+ const manifestContent = await fs.readFile(manifestPath, 'utf8');
2106
+ const yaml = require('yaml');
2107
+ manifest = yaml.parse(manifestContent);
2108
+ installedModules = manifest.modules || [];
2109
+ }
2110
+
2111
+ // Check for custom modules with missing sources
2112
+ if (manifest && manifest.customModules && manifest.customModules.length > 0) {
2113
+ console.log(chalk.yellow('\nChecking custom module sources before compilation...'));
2114
+
2115
+ const customModuleSources = new Map();
2116
+ for (const customModule of manifest.customModules) {
2117
+ customModuleSources.set(customModule.id, customModule);
2118
+ }
2119
+
2120
+ const projectRoot = getProjectRoot();
2121
+ await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules);
2122
+ }
2123
+
2124
+ let agentCount = 0;
2125
+ let taskCount = 0;
2126
+
2127
+ // Process all modules in bmad directory
2128
+ const entries = await fs.readdir(bmadDir, { withFileTypes: true });
2129
+
2130
+ for (const entry of entries) {
2131
+ if (entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs') {
2132
+ const modulePath = path.join(bmadDir, entry.name);
2133
+
2134
+ // Special handling for standalone agents in bmad/agents/ directory
2135
+ if (entry.name === 'agents') {
2136
+ await this.buildStandaloneAgents(bmadDir, projectDir);
2137
+
2138
+ // Count standalone agents
2139
+ const standaloneAgentsPath = path.join(bmadDir, 'agents');
2140
+ const standaloneAgentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
2141
+ for (const agentDir of standaloneAgentDirs) {
2142
+ if (agentDir.isDirectory()) {
2143
+ const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
2144
+ const agentFiles = await fs.readdir(agentDirPath);
2145
+ agentCount += agentFiles.filter((f) => f.endsWith('.md') && !f.endsWith('.agent.yaml')).length;
2146
+ }
2147
+ }
2148
+ } else {
2149
+ // Rebuild module agents from installer source
2150
+ const agentsPath = path.join(modulePath, 'agents');
2151
+ if (await fs.pathExists(agentsPath)) {
2152
+ await this.rebuildAgentFiles(modulePath, entry.name);
2153
+ const agentFiles = await fs.readdir(agentsPath);
2154
+ agentCount += agentFiles.filter((f) => f.endsWith('.md')).length;
2155
+ }
2156
+
2157
+ // Count tasks (already built)
2158
+ const tasksPath = path.join(modulePath, 'tasks');
2159
+ if (await fs.pathExists(tasksPath)) {
2160
+ const taskFiles = await fs.readdir(tasksPath);
2161
+ taskCount += taskFiles.filter((f) => f.endsWith('.md')).length;
2162
+ }
2163
+ }
2164
+ }
2165
+ }
2166
+
2167
+ // Update IDE configurations using the existing IDE list from manifest
2168
+ if (manifest && manifest.ides && manifest.ides.length > 0) {
2169
+ for (const ide of manifest.ides) {
2170
+ await this.ideManager.setup(ide, projectDir, bmadDir, {
2171
+ selectedModules: installedModules,
2172
+ skipModuleInstall: true, // Skip module installation, just update IDE files
2173
+ verbose: config.verbose,
2174
+ preCollectedConfig: { _alreadyConfigured: true }, // Skip all interactive prompts during compile
2175
+ });
2176
+ }
2177
+ console.log(chalk.green('✓ IDE configurations updated'));
2178
+ } else {
2179
+ console.log(chalk.yellow('⚠️ No IDEs configured. Skipping IDE update.'));
2180
+ }
2181
+ return { agentCount, taskCount };
2182
+ } catch (error) {
2183
+ throw error;
2184
+ }
2185
+ }
2186
+
2187
+ /**
2188
+ * Private: Update core
2189
+ */
2190
+ async updateCore(bmadDir, force = false) {
2191
+ const sourcePath = getModulePath('core');
2192
+ const targetPath = path.join(bmadDir, 'core');
2193
+
2194
+ if (force) {
2195
+ await fs.remove(targetPath);
2196
+ await this.installCore(bmadDir);
2197
+ } else {
2198
+ // Selective update - preserve user modifications
2199
+ await this.fileOps.syncDirectory(sourcePath, targetPath);
2200
+
2201
+ // Recompile agents (#1133)
2202
+ const { ModuleManager } = require('../modules/manager');
2203
+ const moduleManager = new ModuleManager();
2204
+ await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir, this);
2205
+ await this.processAgentFiles(targetPath, 'core');
2206
+ }
2207
+ }
2208
+
2209
+ /**
2210
+ * Quick update method - preserves all settings and only prompts for new config fields
2211
+ * @param {Object} config - Configuration with directory
2212
+ * @returns {Object} Update result
2213
+ */
2214
+ async quickUpdate(config) {
2215
+ const ora = require('ora');
2216
+ const spinner = ora('Starting quick update...').start();
2217
+
2218
+ try {
2219
+ const projectDir = path.resolve(config.directory);
2220
+ const { bmadDir } = await this.findBmadDir(projectDir);
2221
+
2222
+ // Check if bmad directory exists
2223
+ if (!(await fs.pathExists(bmadDir))) {
2224
+ spinner.fail('No BMAD installation found');
2225
+ throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
2226
+ }
2227
+
2228
+ spinner.text = 'Detecting installed modules and configuration...';
2229
+
2230
+ // Detect existing installation
2231
+ const existingInstall = await this.detector.detect(bmadDir);
2232
+ const installedModules = existingInstall.modules.map((m) => m.id);
2233
+ const configuredIdes = existingInstall.ides || [];
2234
+ const projectRoot = path.dirname(bmadDir);
2235
+
2236
+ // Get custom module sources from cache
2237
+ const customModuleSources = new Map();
2238
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
2239
+ if (await fs.pathExists(cacheDir)) {
2240
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
2241
+
2242
+ for (const cachedModule of cachedModules) {
2243
+ if (cachedModule.isDirectory()) {
2244
+ const moduleId = cachedModule.name;
2245
+
2246
+ // Skip if we already have this module from manifest
2247
+ if (customModuleSources.has(moduleId)) {
2248
+ continue;
2249
+ }
2250
+
2251
+ const cachedPath = path.join(cacheDir, moduleId);
2252
+
2253
+ // Check if this is actually a custom module (has module.yaml)
2254
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
2255
+ if (await fs.pathExists(moduleYamlPath)) {
2256
+ // For quick update, we always rebuild from cache
2257
+ customModuleSources.set(moduleId, {
2258
+ id: moduleId,
2259
+ name: moduleId, // We'll read the actual name if needed
2260
+ sourcePath: cachedPath,
2261
+ cached: true, // Flag to indicate this is from cache
2262
+ });
2263
+ }
2264
+ }
2265
+ }
2266
+ }
2267
+
2268
+ // Load saved IDE configurations
2269
+ const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
2270
+
2271
+ // Get available modules (what we have source for)
2272
+ const availableModulesData = await this.moduleManager.listAvailable();
2273
+ const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
2274
+
2275
+ // Add custom modules from manifest if their sources exist
2276
+ for (const [moduleId, customModule] of customModuleSources) {
2277
+ // Use the absolute sourcePath
2278
+ const sourcePath = customModule.sourcePath;
2279
+
2280
+ // Check if source exists at the recorded path
2281
+ if (
2282
+ sourcePath &&
2283
+ (await fs.pathExists(sourcePath)) && // Add to available modules if not already there
2284
+ !availableModules.some((m) => m.id === moduleId)
2285
+ ) {
2286
+ availableModules.push({
2287
+ id: moduleId,
2288
+ name: customModule.name || moduleId,
2289
+ path: sourcePath,
2290
+ isCustom: true,
2291
+ fromManifest: true,
2292
+ });
2293
+ }
2294
+ }
2295
+
2296
+ // Handle missing custom module sources using shared method
2297
+ const customModuleResult = await this.handleMissingCustomSources(
2298
+ customModuleSources,
2299
+ bmadDir,
2300
+ projectRoot,
2301
+ 'update',
2302
+ installedModules,
2303
+ );
2304
+
2305
+ const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
2306
+
2307
+ const customModulesFromManifest = validCustomModules.map((m) => ({
2308
+ ...m,
2309
+ isCustom: true,
2310
+ hasUpdate: true,
2311
+ }));
2312
+
2313
+ const allAvailableModules = [...availableModules, ...customModulesFromManifest];
2314
+ const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
2315
+
2316
+ // Core module is special - never include it in update flow
2317
+ const nonCoreInstalledModules = installedModules.filter((id) => id !== 'core');
2318
+
2319
+ // Only update modules that are BOTH installed AND available (we have source for)
2320
+ const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id));
2321
+ const skippedModules = nonCoreInstalledModules.filter((id) => !availableModuleIds.has(id));
2322
+
2323
+ // Add custom modules that were kept without sources to the skipped modules
2324
+ // This ensures their agents are preserved in the manifest
2325
+ for (const keptModule of keptModulesWithoutSources) {
2326
+ if (!skippedModules.includes(keptModule)) {
2327
+ skippedModules.push(keptModule);
2328
+ }
2329
+ }
2330
+
2331
+ spinner.succeed(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`);
2332
+
2333
+ if (skippedModules.length > 0) {
2334
+ console.log(chalk.yellow(`⚠️ Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`));
2335
+ }
2336
+
2337
+ // Load existing configs and collect new fields (if any)
2338
+ console.log(chalk.cyan('\n📋 Checking for new configuration options...'));
2339
+ await this.configCollector.loadExistingConfig(projectDir);
2340
+
2341
+ let promptedForNewFields = false;
2342
+
2343
+ // Check core config for new fields
2344
+ const corePrompted = await this.configCollector.collectModuleConfigQuick('core', projectDir, true);
2345
+ if (corePrompted) {
2346
+ promptedForNewFields = true;
2347
+ }
2348
+
2349
+ // Check each module we're updating for new fields (NOT skipped modules)
2350
+ for (const moduleName of modulesToUpdate) {
2351
+ const modulePrompted = await this.configCollector.collectModuleConfigQuick(moduleName, projectDir, true);
2352
+ if (modulePrompted) {
2353
+ promptedForNewFields = true;
2354
+ }
2355
+ }
2356
+
2357
+ if (!promptedForNewFields) {
2358
+ console.log(chalk.green('✓ All configuration is up to date, no new options to configure'));
2359
+ }
2360
+
2361
+ // Add metadata
2362
+ this.configCollector.collectedConfig._meta = {
2363
+ version: require(path.join(getProjectRoot(), 'package.json')).version,
2364
+ installDate: new Date().toISOString(),
2365
+ lastModified: new Date().toISOString(),
2366
+ };
2367
+
2368
+ // Build the config object for the installer
2369
+ const installConfig = {
2370
+ directory: projectDir,
2371
+ installCore: true,
2372
+ modules: modulesToUpdate, // Only update modules we have source for
2373
+ ides: configuredIdes,
2374
+ skipIde: configuredIdes.length === 0,
2375
+ coreConfig: this.configCollector.collectedConfig.core,
2376
+ actionType: 'install', // Use regular install flow
2377
+ _quickUpdate: true, // Flag to skip certain prompts
2378
+ _preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
2379
+ _savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer
2380
+ _customModuleSources: customModuleSources, // Pass custom module sources for updates
2381
+ _existingModules: installedModules, // Pass all installed modules for manifest generation
2382
+ };
2383
+
2384
+ // Call the standard install method
2385
+ const result = await this.install(installConfig);
2386
+
2387
+ // Only succeed the spinner if it's still spinning
2388
+ // (install method might have stopped it if folder name changed)
2389
+ if (spinner.isSpinning) {
2390
+ spinner.succeed('Quick update complete!');
2391
+ }
2392
+
2393
+ return {
2394
+ success: true,
2395
+ moduleCount: modulesToUpdate.length + 1, // +1 for core
2396
+ hadNewFields: promptedForNewFields,
2397
+ modules: ['core', ...modulesToUpdate],
2398
+ skippedModules: skippedModules,
2399
+ ides: configuredIdes,
2400
+ };
2401
+ } catch (error) {
2402
+ spinner.fail('Quick update failed');
2403
+ throw error;
2404
+ }
2405
+ }
2406
+
2407
+ /**
2408
+ * Compile agents with customizations only
2409
+ * @param {Object} config - Configuration with directory
2410
+ * @returns {Object} Compilation result
2411
+ */
2412
+ async compileAgents(config) {
2413
+ const ora = require('ora');
2414
+ const chalk = require('chalk');
2415
+ const { ModuleManager } = require('../modules/manager');
2416
+ const { getSourcePath } = require('../../../lib/project-root');
2417
+
2418
+ const spinner = ora('Recompiling agents with customizations...').start();
2419
+
2420
+ try {
2421
+ const projectDir = path.resolve(config.directory);
2422
+ const { bmadDir } = await this.findBmadDir(projectDir);
2423
+
2424
+ // Check if bmad directory exists
2425
+ if (!(await fs.pathExists(bmadDir))) {
2426
+ spinner.fail('No BMAD installation found');
2427
+ throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
2428
+ }
2429
+
2430
+ // Detect existing installation
2431
+ const existingInstall = await this.detector.detect(bmadDir);
2432
+ const installedModules = existingInstall.modules.map((m) => m.id);
2433
+
2434
+ // Initialize module manager
2435
+ const moduleManager = new ModuleManager();
2436
+ moduleManager.setBmadFolderName(path.basename(bmadDir));
2437
+
2438
+ let totalAgentCount = 0;
2439
+
2440
+ // Get custom module sources from cache
2441
+ const customModuleSources = new Map();
2442
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
2443
+ if (await fs.pathExists(cacheDir)) {
2444
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
2445
+
2446
+ for (const cachedModule of cachedModules) {
2447
+ if (cachedModule.isDirectory()) {
2448
+ const moduleId = cachedModule.name;
2449
+ const cachedPath = path.join(cacheDir, moduleId);
2450
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
2451
+
2452
+ // Check if this is actually a custom module
2453
+ if (await fs.pathExists(moduleYamlPath)) {
2454
+ customModuleSources.set(moduleId, cachedPath);
2455
+ }
2456
+ }
2457
+ }
2458
+ }
2459
+
2460
+ // Process each installed module
2461
+ for (const moduleId of installedModules) {
2462
+ spinner.text = `Recompiling agents in ${moduleId}...`;
2463
+
2464
+ // Get source path
2465
+ let sourcePath;
2466
+ if (moduleId === 'core') {
2467
+ sourcePath = getSourcePath('core');
2468
+ } else {
2469
+ // First check if it's in the custom cache
2470
+ if (customModuleSources.has(moduleId)) {
2471
+ sourcePath = customModuleSources.get(moduleId);
2472
+ } else {
2473
+ sourcePath = await moduleManager.findModuleSource(moduleId);
2474
+ }
2475
+ }
2476
+
2477
+ if (!sourcePath) {
2478
+ console.log(chalk.yellow(` Warning: Source not found for module ${moduleId}, skipping...`));
2479
+ continue;
2480
+ }
2481
+
2482
+ const targetPath = path.join(bmadDir, moduleId);
2483
+
2484
+ // Compile agents for this module
2485
+ await moduleManager.compileModuleAgents(sourcePath, targetPath, moduleId, bmadDir, this);
2486
+
2487
+ // Count agents (rough estimate based on files)
2488
+ const agentsPath = path.join(targetPath, 'agents');
2489
+ if (await fs.pathExists(agentsPath)) {
2490
+ const agentFiles = await fs.readdir(agentsPath);
2491
+ const agentCount = agentFiles.filter(f => f.endsWith('.md')).length;
2492
+ totalAgentCount += agentCount;
2493
+ }
2494
+ }
2495
+
2496
+ spinner.succeed('Agent recompilation complete!');
2497
+
2498
+ return {
2499
+ success: true,
2500
+ agentCount: totalAgentCount,
2501
+ modules: installedModules,
2502
+ };
2503
+ } catch (error) {
2504
+ spinner.fail('Agent recompilation failed');
2505
+ throw error;
2506
+ }
2507
+ }
2508
+
2509
+ /**
2510
+ * Private: Prompt for update action
2511
+ */
2512
+ async promptUpdateAction() {
2513
+ const inquirer = require('inquirer');
2514
+ return await inquirer.prompt([
2515
+ {
2516
+ type: 'list',
2517
+ name: 'action',
2518
+ message: 'What would you like to do?',
2519
+ choices: [{ name: 'Update existing installation', value: 'update' }],
2520
+ },
2521
+ ]);
2522
+ }
2523
+
2524
+ /**
2525
+ * Handle legacy BMAD v4 migration with automatic backup
2526
+ * @param {string} projectDir - Project directory
2527
+ * @param {Object} legacyV4 - Legacy V4 detection result with offenders array
2528
+ */
2529
+ async handleLegacyV4Migration(projectDir, legacyV4) {
2530
+ console.log(chalk.yellow.bold('\n⚠️ Legacy BMAD v4 detected'));
2531
+ console.log(chalk.dim('The installer found legacy artefacts in your project.\n'));
2532
+
2533
+ // Separate _bmad* folders (auto-backup) from other offending paths (manual cleanup)
2534
+ const bmadFolders = legacyV4.offenders.filter((p) => {
2535
+ const name = path.basename(p);
2536
+ return name.startsWith('_bmad'); // Only dot-prefixed folders get auto-backed up
2537
+ });
2538
+ const otherOffenders = legacyV4.offenders.filter((p) => {
2539
+ const name = path.basename(p);
2540
+ return !name.startsWith('_bmad'); // Everything else is manual cleanup
2541
+ });
2542
+
2543
+ const inquirer = require('inquirer');
2544
+
2545
+ // Show warning for other offending paths FIRST
2546
+ if (otherOffenders.length > 0) {
2547
+ console.log(chalk.yellow('⚠️ Recommended cleanup:'));
2548
+ console.log(chalk.dim('It is recommended to remove the following items before proceeding:\n'));
2549
+ for (const p of otherOffenders) console.log(chalk.dim(` - ${p}`));
2550
+
2551
+ console.log(chalk.cyan('\nCleanup commands you can copy/paste:'));
2552
+ console.log(chalk.dim('macOS/Linux:'));
2553
+ for (const p of otherOffenders) console.log(chalk.dim(` rm -rf '${p}'`));
2554
+ console.log(chalk.dim('Windows:'));
2555
+ for (const p of otherOffenders) console.log(chalk.dim(` rmdir /S /Q "${p}"`));
2556
+
2557
+ const { cleanedUp } = await inquirer.prompt([
2558
+ {
2559
+ type: 'confirm',
2560
+ name: 'cleanedUp',
2561
+ message: 'Have you completed the recommended cleanup? (You can proceed without it, but it is recommended)',
2562
+ default: false,
2563
+ },
2564
+ ]);
2565
+
2566
+ if (cleanedUp) {
2567
+ console.log(chalk.green('✓ Cleanup acknowledged\n'));
2568
+ } else {
2569
+ console.log(chalk.yellow('⚠️ Proceeding without recommended cleanup\n'));
2570
+ }
2571
+ }
2572
+
2573
+ // Handle _bmad* folders with automatic backup
2574
+ if (bmadFolders.length > 0) {
2575
+ console.log(chalk.cyan('The following legacy folders will be moved to v4-backup:'));
2576
+ for (const p of bmadFolders) console.log(chalk.dim(` - ${p}`));
2577
+
2578
+ const { proceed } = await inquirer.prompt([
2579
+ {
2580
+ type: 'confirm',
2581
+ name: 'proceed',
2582
+ message: 'Proceed with backing up legacy v4 folders?',
2583
+ default: true,
2584
+ },
2585
+ ]);
2586
+
2587
+ if (proceed) {
2588
+ const backupDir = path.join(projectDir, 'v4-backup');
2589
+ await fs.ensureDir(backupDir);
2590
+
2591
+ for (const folder of bmadFolders) {
2592
+ const folderName = path.basename(folder);
2593
+ const backupPath = path.join(backupDir, folderName);
2594
+
2595
+ // If backup already exists, add timestamp
2596
+ let finalBackupPath = backupPath;
2597
+ if (await fs.pathExists(backupPath)) {
2598
+ const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-').split('T')[0];
2599
+ finalBackupPath = path.join(backupDir, `${folderName}-${timestamp}`);
2600
+ }
2601
+
2602
+ await fs.move(folder, finalBackupPath, { overwrite: false });
2603
+ console.log(chalk.green(`✓ Moved ${folderName} to ${path.relative(projectDir, finalBackupPath)}`));
2604
+ }
2605
+ } else {
2606
+ throw new Error('Installation cancelled by user');
2607
+ }
2608
+ }
2609
+ }
2610
+
2611
+ /**
2612
+ * Read files-manifest.csv
2613
+ * @param {string} bmadDir - BMAD installation directory
2614
+ * @returns {Array} Array of file entries from files-manifest.csv
2615
+ */
2616
+ async readFilesManifest(bmadDir) {
2617
+ const filesManifestPath = path.join(bmadDir, '_config', 'files-manifest.csv');
2618
+ if (!(await fs.pathExists(filesManifestPath))) {
2619
+ return [];
2620
+ }
2621
+
2622
+ try {
2623
+ const content = await fs.readFile(filesManifestPath, 'utf8');
2624
+ const lines = content.split('\n');
2625
+ const files = [];
2626
+
2627
+ for (let i = 1; i < lines.length; i++) {
2628
+ // Skip header
2629
+ const line = lines[i].trim();
2630
+ if (!line) continue;
2631
+
2632
+ // Parse CSV line properly handling quoted values
2633
+ const parts = [];
2634
+ let current = '';
2635
+ let inQuotes = false;
2636
+
2637
+ for (const char of line) {
2638
+ if (char === '"') {
2639
+ inQuotes = !inQuotes;
2640
+ } else if (char === ',' && !inQuotes) {
2641
+ parts.push(current);
2642
+ current = '';
2643
+ } else {
2644
+ current += char;
2645
+ }
2646
+ }
2647
+ parts.push(current); // Add last part
2648
+
2649
+ if (parts.length >= 4) {
2650
+ files.push({
2651
+ type: parts[0],
2652
+ name: parts[1],
2653
+ module: parts[2],
2654
+ path: parts[3],
2655
+ hash: parts[4] || null, // Hash may not exist in old manifests
2656
+ });
2657
+ }
2658
+ }
2659
+
2660
+ return files;
2661
+ } catch (error) {
2662
+ console.warn('Warning: Could not read files-manifest.csv:', error.message);
2663
+ return [];
2664
+ }
2665
+ }
2666
+
2667
+ /**
2668
+ * Detect custom and modified files
2669
+ * @param {string} bmadDir - BMAD installation directory
2670
+ * @param {Array} existingFilesManifest - Previous files from files-manifest.csv
2671
+ * @returns {Object} Object with customFiles and modifiedFiles arrays
2672
+ */
2673
+ async detectCustomFiles(bmadDir, existingFilesManifest) {
2674
+ const customFiles = [];
2675
+ const modifiedFiles = [];
2676
+
2677
+ // Memory is always in _bmad/_memory
2678
+ const bmadMemoryPath = '_memory';
2679
+
2680
+ // Check if the manifest has hashes - if not, we can't detect modifications
2681
+ let manifestHasHashes = false;
2682
+ if (existingFilesManifest && existingFilesManifest.length > 0) {
2683
+ manifestHasHashes = existingFilesManifest.some((f) => f.hash);
2684
+ }
2685
+
2686
+ // Build map of previously installed files from files-manifest.csv with their hashes
2687
+ const installedFilesMap = new Map();
2688
+ for (const fileEntry of existingFilesManifest) {
2689
+ if (fileEntry.path) {
2690
+ const absolutePath = path.join(bmadDir, fileEntry.path);
2691
+ installedFilesMap.set(path.normalize(absolutePath), {
2692
+ hash: fileEntry.hash,
2693
+ relativePath: fileEntry.path,
2694
+ });
2695
+ }
2696
+ }
2697
+
2698
+ // Recursively scan bmadDir for all files
2699
+ const scanDirectory = async (dir) => {
2700
+ try {
2701
+ const entries = await fs.readdir(dir, { withFileTypes: true });
2702
+ for (const entry of entries) {
2703
+ const fullPath = path.join(dir, entry.name);
2704
+
2705
+ if (entry.isDirectory()) {
2706
+ // Skip certain directories
2707
+ if (entry.name === 'node_modules' || entry.name === '.git') {
2708
+ continue;
2709
+ }
2710
+ await scanDirectory(fullPath);
2711
+ } else if (entry.isFile()) {
2712
+ const normalizedPath = path.normalize(fullPath);
2713
+ const fileInfo = installedFilesMap.get(normalizedPath);
2714
+
2715
+ // Skip certain system files that are auto-generated
2716
+ const relativePath = path.relative(bmadDir, fullPath);
2717
+ const fileName = path.basename(fullPath);
2718
+
2719
+ // Skip _config directory EXCEPT for modified agent customizations
2720
+ if (relativePath.startsWith('_config/') || relativePath.startsWith('_config\\')) {
2721
+ // Special handling for .customize.yaml files - only preserve if modified
2722
+ if (relativePath.includes('/agents/') && fileName.endsWith('.customize.yaml')) {
2723
+ // Check if the customization file has been modified from manifest
2724
+ const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
2725
+ if (await fs.pathExists(manifestPath)) {
2726
+ const crypto = require('node:crypto');
2727
+ const currentContent = await fs.readFile(fullPath, 'utf8');
2728
+ const currentHash = crypto.createHash('sha256').update(currentContent).digest('hex');
2729
+
2730
+ const yaml = require('yaml');
2731
+ const manifestContent = await fs.readFile(manifestPath, 'utf8');
2732
+ const manifestData = yaml.parse(manifestContent);
2733
+ const originalHash = manifestData.agentCustomizations?.[relativePath];
2734
+
2735
+ // Only add to customFiles if hash differs (user modified)
2736
+ if (originalHash && currentHash !== originalHash) {
2737
+ customFiles.push(fullPath);
2738
+ }
2739
+ }
2740
+ }
2741
+ continue;
2742
+ }
2743
+
2744
+ if (relativePath.startsWith(bmadMemoryPath + '/') && path.dirname(relativePath).includes('-sidecar')) {
2745
+ continue;
2746
+ }
2747
+
2748
+ // Skip config.yaml files - these are regenerated on each install/update
2749
+ if (fileName === 'config.yaml') {
2750
+ continue;
2751
+ }
2752
+
2753
+ if (!fileInfo) {
2754
+ // File not in manifest = custom file
2755
+ // EXCEPT: Agent .md files in module folders are generated files, not custom
2756
+ // Only treat .md files under _config/agents/ as custom
2757
+ if (!(fileName.endsWith('.md') && relativePath.includes('/agents/') && !relativePath.startsWith('_config/'))) {
2758
+ customFiles.push(fullPath);
2759
+ }
2760
+ } else if (manifestHasHashes && fileInfo.hash) {
2761
+ // File in manifest with hash - check if it was modified
2762
+ const currentHash = await this.manifest.calculateFileHash(fullPath);
2763
+ if (currentHash && currentHash !== fileInfo.hash) {
2764
+ // Hash changed = file was modified
2765
+ modifiedFiles.push({
2766
+ path: fullPath,
2767
+ relativePath: fileInfo.relativePath,
2768
+ });
2769
+ }
2770
+ }
2771
+ }
2772
+ }
2773
+ } catch {
2774
+ // Ignore errors scanning directories
2775
+ }
2776
+ };
2777
+
2778
+ await scanDirectory(bmadDir);
2779
+ return { customFiles, modifiedFiles };
2780
+ }
2781
+
2782
+ /**
2783
+ * Private: Create agent configuration files
2784
+ * @param {string} bmadDir - BMAD installation directory
2785
+ * @param {Object} userInfo - User information including name and language
2786
+ */
2787
+ async createAgentConfigs(bmadDir, userInfo = null) {
2788
+ const agentConfigDir = path.join(bmadDir, '_config', 'agents');
2789
+ await fs.ensureDir(agentConfigDir);
2790
+
2791
+ // Get all agents from all modules
2792
+ const agents = [];
2793
+ const agentDetails = []; // For manifest generation
2794
+
2795
+ // Check modules for agents (including core)
2796
+ const entries = await fs.readdir(bmadDir, { withFileTypes: true });
2797
+ for (const entry of entries) {
2798
+ if (entry.isDirectory() && entry.name !== '_config') {
2799
+ const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents');
2800
+ if (await fs.pathExists(moduleAgentsPath)) {
2801
+ const agentFiles = await fs.readdir(moduleAgentsPath);
2802
+ for (const agentFile of agentFiles) {
2803
+ if (agentFile.endsWith('.md')) {
2804
+ const agentPath = path.join(moduleAgentsPath, agentFile);
2805
+ const agentContent = await fs.readFile(agentPath, 'utf8');
2806
+
2807
+ // Skip agents with localskip="true"
2808
+ const hasLocalSkip = agentContent.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
2809
+ if (hasLocalSkip) {
2810
+ continue; // Skip this agent - it should not have been installed
2811
+ }
2812
+
2813
+ const agentName = path.basename(agentFile, '.md');
2814
+
2815
+ // Extract any nodes with agentConfig="true"
2816
+ const agentConfigNodes = this.extractAgentConfigNodes(agentContent);
2817
+
2818
+ agents.push({
2819
+ name: agentName,
2820
+ module: entry.name,
2821
+ agentConfigNodes: agentConfigNodes,
2822
+ });
2823
+
2824
+ // Use shared AgentPartyGenerator to extract details
2825
+ let details = AgentPartyGenerator.extractAgentDetails(agentContent, entry.name, agentName);
2826
+
2827
+ // Apply config overrides if they exist
2828
+ if (details) {
2829
+ const configPath = path.join(agentConfigDir, `${entry.name}-${agentName}.md`);
2830
+ if (await fs.pathExists(configPath)) {
2831
+ const configContent = await fs.readFile(configPath, 'utf8');
2832
+ details = AgentPartyGenerator.applyConfigOverrides(details, configContent);
2833
+ }
2834
+ agentDetails.push(details);
2835
+ }
2836
+ }
2837
+ }
2838
+ }
2839
+ }
2840
+ }
2841
+
2842
+ // Create config file for each agent
2843
+ let createdCount = 0;
2844
+ let skippedCount = 0;
2845
+
2846
+ // Load agent config template
2847
+ const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md');
2848
+ const templateContent = await fs.readFile(templatePath, 'utf8');
2849
+
2850
+ for (const agent of agents) {
2851
+ const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`);
2852
+
2853
+ // Skip if config file already exists (preserve custom configurations)
2854
+ if (await fs.pathExists(configPath)) {
2855
+ skippedCount++;
2856
+ continue;
2857
+ }
2858
+
2859
+ // Build config content header
2860
+ let configContent = `# Agent Config: ${agent.name}\n\n`;
2861
+
2862
+ // Process template and add agent-specific config nodes
2863
+ let processedTemplate = templateContent;
2864
+
2865
+ // Replace {core:user_name} placeholder with actual user name if available
2866
+ if (userInfo && userInfo.userName) {
2867
+ processedTemplate = processedTemplate.replaceAll('{core:user_name}', userInfo.userName);
2868
+ }
2869
+
2870
+ // Replace {core:communication_language} placeholder with actual language if available
2871
+ if (userInfo && userInfo.responseLanguage) {
2872
+ processedTemplate = processedTemplate.replaceAll('{core:communication_language}', userInfo.responseLanguage);
2873
+ }
2874
+
2875
+ // If this agent has agentConfig nodes, add them after the existing comment
2876
+ if (agent.agentConfigNodes && agent.agentConfigNodes.length > 0) {
2877
+ // Find the agent-specific configuration nodes comment
2878
+ const commentPattern = /(\s*<!-- Agent-specific configuration nodes -->)/;
2879
+ const commentMatch = processedTemplate.match(commentPattern);
2880
+
2881
+ if (commentMatch) {
2882
+ // Add nodes right after the comment
2883
+ let agentSpecificNodes = '';
2884
+ for (const node of agent.agentConfigNodes) {
2885
+ agentSpecificNodes += `\n ${node}`;
2886
+ }
2887
+
2888
+ processedTemplate = processedTemplate.replace(commentPattern, `$1${agentSpecificNodes}`);
2889
+ }
2890
+ }
2891
+
2892
+ configContent += processedTemplate;
2893
+
2894
+ // Ensure POSIX-compliant final newline
2895
+ if (!configContent.endsWith('\n')) {
2896
+ configContent += '\n';
2897
+ }
2898
+
2899
+ await fs.writeFile(configPath, configContent, 'utf8');
2900
+ this.installedFiles.add(configPath); // Track agent config files
2901
+ createdCount++;
2902
+ }
2903
+
2904
+ // Generate agent manifest with overrides applied
2905
+ await this.generateAgentManifest(bmadDir, agentDetails);
2906
+
2907
+ return { total: agents.length, created: createdCount, skipped: skippedCount };
2908
+ }
2909
+
2910
+ /**
2911
+ * Generate agent manifest XML file
2912
+ * @param {string} bmadDir - BMAD installation directory
2913
+ * @param {Array} agentDetails - Array of agent details
2914
+ */
2915
+ async generateAgentManifest(bmadDir, agentDetails) {
2916
+ const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
2917
+ await AgentPartyGenerator.writeAgentParty(manifestPath, agentDetails, { forWeb: false });
2918
+ }
2919
+
2920
+ /**
2921
+ * Extract nodes with agentConfig="true" from agent content
2922
+ * @param {string} content - Agent file content
2923
+ * @returns {Array} Array of XML nodes that should be added to agent config
2924
+ */
2925
+ extractAgentConfigNodes(content) {
2926
+ const nodes = [];
2927
+
2928
+ try {
2929
+ // Find all XML nodes with agentConfig="true"
2930
+ // Match self-closing tags and tags with content
2931
+ const selfClosingPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*\/>/g;
2932
+ const withContentPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*>([\s\S]*?)<\/\1>/g;
2933
+
2934
+ // Extract self-closing tags
2935
+ let match;
2936
+ while ((match = selfClosingPattern.exec(content)) !== null) {
2937
+ // Extract just the tag without children (structure only)
2938
+ const tagMatch = match[0].match(/<([a-zA-Z][a-zA-Z0-9_-]*)([^>]*)\/>/);
2939
+ if (tagMatch) {
2940
+ const tagName = tagMatch[1];
2941
+ const attributes = tagMatch[2].replace(/\s*agentConfig="true"/, ''); // Remove agentConfig attribute
2942
+ nodes.push(`<${tagName}${attributes}></${tagName}>`);
2943
+ }
2944
+ }
2945
+
2946
+ // Extract tags with content
2947
+ while ((match = withContentPattern.exec(content)) !== null) {
2948
+ const fullMatch = match[0];
2949
+ const tagName = match[1];
2950
+
2951
+ // Extract opening tag with attributes (removing agentConfig="true")
2952
+ const openingTagMatch = fullMatch.match(new RegExp(`<${tagName}([^>]*)>`));
2953
+ if (openingTagMatch) {
2954
+ const attributes = openingTagMatch[1].replace(/\s*agentConfig="true"/, '');
2955
+ // Add empty node structure (no children)
2956
+ nodes.push(`<${tagName}${attributes}></${tagName}>`);
2957
+ }
2958
+ }
2959
+ } catch (error) {
2960
+ console.error('Error extracting agentConfig nodes:', error);
2961
+ }
2962
+
2963
+ return nodes;
2964
+ }
2965
+
2966
+ /**
2967
+ * Handle missing custom module sources interactively
2968
+ * @param {Map} customModuleSources - Map of custom module ID to info
2969
+ * @param {string} bmadDir - BMAD directory
2970
+ * @param {string} projectRoot - Project root directory
2971
+ * @param {string} operation - Current operation ('update', 'compile', etc.)
2972
+ * @param {Array} installedModules - Array of installed module IDs (will be modified)
2973
+ * @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
2974
+ */
2975
+ async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules) {
2976
+ const validCustomModules = [];
2977
+ const keptModulesWithoutSources = []; // Track modules kept without sources
2978
+ const customModulesWithMissingSources = [];
2979
+
2980
+ // Check which sources exist
2981
+ for (const [moduleId, customInfo] of customModuleSources) {
2982
+ if (await fs.pathExists(customInfo.sourcePath)) {
2983
+ validCustomModules.push({
2984
+ id: moduleId,
2985
+ name: customInfo.name,
2986
+ path: customInfo.sourcePath,
2987
+ info: customInfo,
2988
+ });
2989
+ } else {
2990
+ // For cached modules that are missing, we just skip them without prompting
2991
+ if (customInfo.cached) {
2992
+ // Skip cached modules without prompting
2993
+ keptModulesWithoutSources.push({
2994
+ id: moduleId,
2995
+ name: customInfo.name,
2996
+ cached: true,
2997
+ });
2998
+ } else {
2999
+ customModulesWithMissingSources.push({
3000
+ id: moduleId,
3001
+ name: customInfo.name,
3002
+ sourcePath: customInfo.sourcePath,
3003
+ relativePath: customInfo.relativePath,
3004
+ info: customInfo,
3005
+ });
3006
+ }
3007
+ }
3008
+ }
3009
+
3010
+ // If no missing sources, return immediately
3011
+ if (customModulesWithMissingSources.length === 0) {
3012
+ return {
3013
+ validCustomModules,
3014
+ keptModulesWithoutSources: [],
3015
+ };
3016
+ }
3017
+
3018
+ // Stop any spinner for interactive prompts
3019
+ const currentSpinner = ora();
3020
+ if (currentSpinner.isSpinning) {
3021
+ currentSpinner.stop();
3022
+ }
3023
+
3024
+ console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
3025
+
3026
+ const inquirer = require('inquirer');
3027
+ let keptCount = 0;
3028
+ let updatedCount = 0;
3029
+ let removedCount = 0;
3030
+
3031
+ for (const missing of customModulesWithMissingSources) {
3032
+ console.log(chalk.dim(` • ${missing.name} (${missing.id})`));
3033
+ console.log(chalk.dim(` Original source: ${missing.relativePath}`));
3034
+ console.log(chalk.dim(` Full path: ${missing.sourcePath}`));
3035
+
3036
+ const choices = [
3037
+ {
3038
+ name: 'Keep installed (will not be processed)',
3039
+ value: 'keep',
3040
+ short: 'Keep',
3041
+ },
3042
+ {
3043
+ name: 'Specify new source location',
3044
+ value: 'update',
3045
+ short: 'Update',
3046
+ },
3047
+ ];
3048
+
3049
+ // Only add remove option if not just compiling agents
3050
+ if (operation !== 'compile-agents') {
3051
+ choices.push({
3052
+ name: '⚠️ REMOVE module completely (destructive!)',
3053
+ value: 'remove',
3054
+ short: 'Remove',
3055
+ });
3056
+ }
3057
+
3058
+ const { action } = await inquirer.prompt([
3059
+ {
3060
+ type: 'list',
3061
+ name: 'action',
3062
+ message: `How would you like to handle "${missing.name}"?`,
3063
+ choices,
3064
+ },
3065
+ ]);
3066
+
3067
+ switch (action) {
3068
+ case 'update': {
3069
+ const { newSourcePath } = await inquirer.prompt([
3070
+ {
3071
+ type: 'input',
3072
+ name: 'newSourcePath',
3073
+ message: 'Enter the new path to the custom module:',
3074
+ default: missing.sourcePath,
3075
+ validate: async (input) => {
3076
+ if (!input || input.trim() === '') {
3077
+ return 'Please enter a path';
3078
+ }
3079
+ const expandedPath = path.resolve(input.trim());
3080
+ if (!(await fs.pathExists(expandedPath))) {
3081
+ return 'Path does not exist';
3082
+ }
3083
+ // Check if it looks like a valid module
3084
+ const moduleYamlPath = path.join(expandedPath, 'module.yaml');
3085
+ const agentsPath = path.join(expandedPath, 'agents');
3086
+ const workflowsPath = path.join(expandedPath, 'workflows');
3087
+
3088
+ if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) {
3089
+ return 'Path does not appear to contain a valid custom module';
3090
+ }
3091
+ return true;
3092
+ },
3093
+ },
3094
+ ]);
3095
+
3096
+ // Update the source in manifest
3097
+ const resolvedPath = path.resolve(newSourcePath.trim());
3098
+ missing.info.sourcePath = resolvedPath;
3099
+ // Remove relativePath - we only store absolute sourcePath now
3100
+ delete missing.info.relativePath;
3101
+ await this.manifest.addCustomModule(bmadDir, missing.info);
3102
+
3103
+ validCustomModules.push({
3104
+ id: moduleId,
3105
+ name: missing.name,
3106
+ path: resolvedPath,
3107
+ info: missing.info,
3108
+ });
3109
+
3110
+ updatedCount++;
3111
+ console.log(chalk.green(`✓ Updated source location`));
3112
+
3113
+ break;
3114
+ }
3115
+ case 'remove': {
3116
+ // Extra confirmation for destructive remove
3117
+ console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
3118
+ console.log(chalk.red(` Module location: ${path.join(bmadDir, moduleId)}`));
3119
+
3120
+ const { confirm } = await inquirer.prompt([
3121
+ {
3122
+ type: 'confirm',
3123
+ name: 'confirm',
3124
+ message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
3125
+ default: false,
3126
+ },
3127
+ ]);
3128
+
3129
+ if (confirm) {
3130
+ const { typedConfirm } = await inquirer.prompt([
3131
+ {
3132
+ type: 'input',
3133
+ name: 'typedConfirm',
3134
+ message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
3135
+ validate: (input) => {
3136
+ if (input !== 'DELETE') {
3137
+ return chalk.red('You must type "DELETE" exactly to proceed');
3138
+ }
3139
+ return true;
3140
+ },
3141
+ },
3142
+ ]);
3143
+
3144
+ if (typedConfirm === 'DELETE') {
3145
+ // Remove the module from filesystem and manifest
3146
+ const modulePath = path.join(bmadDir, moduleId);
3147
+ if (await fs.pathExists(modulePath)) {
3148
+ const fsExtra = require('fs-extra');
3149
+ await fsExtra.remove(modulePath);
3150
+ console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
3151
+ }
3152
+
3153
+ await this.manifest.removeModule(bmadDir, moduleId);
3154
+ await this.manifest.removeCustomModule(bmadDir, moduleId);
3155
+ console.log(chalk.yellow(` ✓ Removed from manifest`));
3156
+
3157
+ // Also remove from installedModules list
3158
+ if (installedModules && installedModules.includes(moduleId)) {
3159
+ const index = installedModules.indexOf(moduleId);
3160
+ if (index !== -1) {
3161
+ installedModules.splice(index, 1);
3162
+ }
3163
+ }
3164
+
3165
+ removedCount++;
3166
+ console.log(chalk.red.bold(`✓ "${missing.name}" has been permanently removed`));
3167
+ } else {
3168
+ console.log(chalk.dim(' Removal cancelled - module will be kept'));
3169
+ keptCount++;
3170
+ }
3171
+ } else {
3172
+ console.log(chalk.dim(' Removal cancelled - module will be kept'));
3173
+ keptCount++;
3174
+ }
3175
+
3176
+ break;
3177
+ }
3178
+ case 'keep': {
3179
+ keptCount++;
3180
+ keptModulesWithoutSources.push(moduleId);
3181
+ console.log(chalk.dim(` Module will be kept as-is`));
3182
+
3183
+ break;
3184
+ }
3185
+ // No default
3186
+ }
3187
+ }
3188
+
3189
+ // Show summary
3190
+ if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
3191
+ console.log(chalk.dim(`\nSummary for custom modules with missing sources:`));
3192
+ if (keptCount > 0) console.log(chalk.dim(` • ${keptCount} module(s) kept as-is`));
3193
+ if (updatedCount > 0) console.log(chalk.dim(` • ${updatedCount} module(s) updated with new sources`));
3194
+ if (removedCount > 0) console.log(chalk.red(` • ${removedCount} module(s) permanently deleted`));
3195
+ }
3196
+
3197
+ return {
3198
+ validCustomModules,
3199
+ keptModulesWithoutSources,
3200
+ };
3201
+ }
3202
+ }
3203
+
3204
+ module.exports = { Installer };