bmad-elsabro 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (572) hide show
  1. package/.auto-claude-security.json +209 -0
  2. package/.claude_settings.json +34 -0
  3. package/.coderabbit.yaml +40 -0
  4. package/.nvmrc +1 -0
  5. package/.prettierignore +9 -0
  6. package/AI_TEST_GENERATOR_VERIFICATION.md +260 -0
  7. package/BUDGET_ALERT_TESTING.md +325 -0
  8. package/CHANGELOG.md +1488 -0
  9. package/CNAME +1 -0
  10. package/CONTRIBUTING.md +167 -0
  11. package/CONTRIBUTORS.md +32 -0
  12. package/LEGACY_MIGRATION_TESTING.md +428 -0
  13. package/LICENSE +30 -0
  14. package/PARTY_MODE_VERIFICATION.md +274 -0
  15. package/PROJECT_DETECTOR_TESTING.md +288 -0
  16. package/README.md +236 -0
  17. package/SECURITY.md +85 -0
  18. package/TRADEMARK.md +55 -0
  19. package/WORKFLOW_ROUTER_IMPLEMENTATION.md +379 -0
  20. package/Wordmark.png +0 -0
  21. package/banner-bmad-method.png +0 -0
  22. package/build/icons/512x512.png +0 -0
  23. package/build-mac.sh +3 -0
  24. package/docs/404.md +9 -0
  25. package/docs/_STYLE_GUIDE.md +367 -0
  26. package/docs/developer/api-reference.md +945 -0
  27. package/docs/developer/architecture.md +563 -0
  28. package/docs/developer/contributing.md +831 -0
  29. package/docs/downloads.md +74 -0
  30. package/docs/explanation/advanced-elicitation.md +24 -0
  31. package/docs/explanation/adversarial-review.md +57 -0
  32. package/docs/explanation/brainstorming.md +31 -0
  33. package/docs/explanation/brownfield-faq.md +55 -0
  34. package/docs/explanation/party-mode.md +57 -0
  35. package/docs/explanation/preventing-agent-conflicts.md +110 -0
  36. package/docs/explanation/quick-flow.md +27 -0
  37. package/docs/explanation/why-solutioning-matters.md +75 -0
  38. package/docs/how-to/brownfield/index.md +84 -0
  39. package/docs/how-to/brownfield/quick-fix-in-brownfield.md +76 -0
  40. package/docs/how-to/customize-bmad.md +158 -0
  41. package/docs/how-to/get-answers-about-bmad.md +102 -0
  42. package/docs/how-to/install-bmad.md +82 -0
  43. package/docs/how-to/shard-large-documents.md +101 -0
  44. package/docs/how-to/upgrade-to-v6.md +131 -0
  45. package/docs/index.md +56 -0
  46. package/docs/reference/workflow-map.md +83 -0
  47. package/docs/tea/explanation/engagement-models.md +710 -0
  48. package/docs/tea/explanation/fixture-architecture.md +457 -0
  49. package/docs/tea/explanation/knowledge-base-system.md +554 -0
  50. package/docs/tea/explanation/network-first-patterns.md +853 -0
  51. package/docs/tea/explanation/risk-based-testing.md +586 -0
  52. package/docs/tea/explanation/tea-overview.md +410 -0
  53. package/docs/tea/explanation/test-quality-standards.md +907 -0
  54. package/docs/tea/explanation/testing-as-engineering.md +112 -0
  55. package/docs/tea/glossary/index.md +159 -0
  56. package/docs/tea/how-to/brownfield/use-tea-for-enterprise.md +525 -0
  57. package/docs/tea/how-to/brownfield/use-tea-with-existing-tests.md +577 -0
  58. package/docs/tea/how-to/customization/enable-tea-mcp-enhancements.md +424 -0
  59. package/docs/tea/how-to/customization/integrate-playwright-utils.md +813 -0
  60. package/docs/tea/how-to/workflows/run-atdd.md +436 -0
  61. package/docs/tea/how-to/workflows/run-automate.md +653 -0
  62. package/docs/tea/how-to/workflows/run-nfr-assess.md +679 -0
  63. package/docs/tea/how-to/workflows/run-test-design.md +135 -0
  64. package/docs/tea/how-to/workflows/run-test-review.md +605 -0
  65. package/docs/tea/how-to/workflows/run-trace.md +883 -0
  66. package/docs/tea/how-to/workflows/setup-ci.md +712 -0
  67. package/docs/tea/how-to/workflows/setup-test-framework.md +98 -0
  68. package/docs/tea/reference/commands.md +276 -0
  69. package/docs/tea/reference/configuration.md +678 -0
  70. package/docs/tea/reference/knowledge-base.md +340 -0
  71. package/docs/tea/tutorials/tea-lite-quickstart.md +444 -0
  72. package/docs/tutorials/getting-started.md +205 -0
  73. package/docs/user-guide/getting-started.md +348 -0
  74. package/docs/user-guide/token-economy.md +601 -0
  75. package/docs/user-guide/workflows.md +546 -0
  76. package/electron-builder.yml +75 -0
  77. package/eslint.config.mjs +152 -0
  78. package/package.json +162 -0
  79. package/prettier.config.mjs +32 -0
  80. package/public/monaco-workers/index.js +21 -0
  81. package/renderer/App.tsx +311 -0
  82. package/renderer/components/ChatPanel.tsx +285 -0
  83. package/renderer/components/CodeEditor.tsx +327 -0
  84. package/renderer/components/CodeEditor.types.ts +245 -0
  85. package/renderer/components/FlowSelector.tsx +534 -0
  86. package/renderer/components/MessageInput.tsx +252 -0
  87. package/renderer/components/MessageList.tsx +204 -0
  88. package/renderer/components/MigrationWizard.tsx +896 -0
  89. package/renderer/components/NotificationCenter.tsx +291 -0
  90. package/renderer/components/OnboardingWizard.tsx +112 -0
  91. package/renderer/components/PartyMode.tsx +555 -0
  92. package/renderer/components/Sidebar.module.css +258 -0
  93. package/renderer/components/Sidebar.tsx +157 -0
  94. package/renderer/components/TemplateSelector.tsx +553 -0
  95. package/renderer/components/Terminal.tsx +523 -0
  96. package/renderer/components/TestCenter.tsx +364 -0
  97. package/renderer/components/TokenAnalytics.tsx +607 -0
  98. package/renderer/components/TokenMonitor.tsx +331 -0
  99. package/renderer/components/TutorialOverlay.tsx +483 -0
  100. package/renderer/components/WorkflowEditor.tsx +470 -0
  101. package/renderer/components/onboarding/Step1Welcome.tsx +72 -0
  102. package/renderer/components/onboarding/Step2Setup.tsx +193 -0
  103. package/renderer/components/onboarding/Step3CreateProject.tsx +209 -0
  104. package/renderer/components/test-center/CoverageDashboard.tsx +588 -0
  105. package/renderer/components/test-center/ELI5Guide.tsx +521 -0
  106. package/renderer/components/test-center/TestList.tsx +381 -0
  107. package/renderer/components/test-center/TestRunner.tsx +431 -0
  108. package/renderer/components/test-center/TestStepWizard.tsx +1000 -0
  109. package/renderer/components/test-center/VisualTestBuilder.tsx +460 -0
  110. package/renderer/components/workflow/DependencyEdge.tsx +200 -0
  111. package/renderer/components/workflow/StepNode.tsx +234 -0
  112. package/renderer/components/workflow/StepPalette.tsx +412 -0
  113. package/renderer/context/ThemeContext.tsx +97 -0
  114. package/renderer/data/shortcuts.json +94 -0
  115. package/renderer/data/testing-guides.json +261 -0
  116. package/renderer/data/tutorials.json +546 -0
  117. package/renderer/hooks/useKeyboardShortcuts.ts +249 -0
  118. package/renderer/hooks/useNotifications.ts +267 -0
  119. package/renderer/hooks/useTheme.ts +149 -0
  120. package/renderer/hooks/useTokenTracking.ts +464 -0
  121. package/renderer/hooks/useWorkflowState.ts +309 -0
  122. package/renderer/index.html +16 -0
  123. package/renderer/index.tsx +17 -0
  124. package/renderer/lib/MONACO_OFFLINE_CONFIG.md +153 -0
  125. package/renderer/lib/chart-utils.ts +472 -0
  126. package/renderer/lib/file-system-provider.ts +295 -0
  127. package/renderer/lib/monaco-loader.ts +247 -0
  128. package/renderer/renderer/components/NOTIFICATION_SYSTEM.md +192 -0
  129. package/renderer/styles.css +55 -0
  130. package/renderer/types/css-modules.d.ts +21 -0
  131. package/renderer/types/electron.d.ts +316 -0
  132. package/src/bmm/_module-installer/installer.js +48 -0
  133. package/src/bmm/agents/analyst.agent.yaml +36 -0
  134. package/src/bmm/agents/architect.agent.yaml +28 -0
  135. package/src/bmm/agents/dev.agent.yaml +38 -0
  136. package/src/bmm/agents/parallel-orchestrator.agent.yaml +50 -0
  137. package/src/bmm/agents/pm.agent.yaml +46 -0
  138. package/src/bmm/agents/quick-flow-solo-dev.agent.yaml +32 -0
  139. package/src/bmm/agents/sm.agent.yaml +36 -0
  140. package/src/bmm/agents/tea.agent.yaml +63 -0
  141. package/src/bmm/agents/tech-writer/tech-writer-sidecar/documentation-standards.md +224 -0
  142. package/src/bmm/agents/tech-writer/tech-writer.agent.yaml +45 -0
  143. package/src/bmm/agents/ux-designer.agent.yaml +26 -0
  144. package/src/bmm/agents/yolo-dev.agent.yaml +41 -0
  145. package/src/bmm/data/auto-testing-config.yaml +84 -0
  146. package/src/bmm/data/guided-mode-instructions.yaml +112 -0
  147. package/src/bmm/data/parallelization-config.yaml +136 -0
  148. package/src/bmm/data/project-context-template.md +26 -0
  149. package/src/bmm/data/speed-profiles.yaml +127 -0
  150. package/src/bmm/module-help.csv +32 -0
  151. package/src/bmm/module.yaml +60 -0
  152. package/src/bmm/teams/default-party.csv +21 -0
  153. package/src/bmm/teams/team-fullstack.yaml +12 -0
  154. package/src/bmm/testarch/knowledge/adr-quality-readiness-checklist.md +350 -0
  155. package/src/bmm/testarch/knowledge/api-request.md +442 -0
  156. package/src/bmm/testarch/knowledge/api-testing-patterns.md +843 -0
  157. package/src/bmm/testarch/knowledge/auth-session.md +552 -0
  158. package/src/bmm/testarch/knowledge/burn-in.md +273 -0
  159. package/src/bmm/testarch/knowledge/ci-burn-in.md +675 -0
  160. package/src/bmm/testarch/knowledge/component-tdd.md +486 -0
  161. package/src/bmm/testarch/knowledge/contract-testing.md +957 -0
  162. package/src/bmm/testarch/knowledge/data-factories.md +500 -0
  163. package/src/bmm/testarch/knowledge/email-auth.md +721 -0
  164. package/src/bmm/testarch/knowledge/error-handling.md +725 -0
  165. package/src/bmm/testarch/knowledge/feature-flags.md +750 -0
  166. package/src/bmm/testarch/knowledge/file-utils.md +463 -0
  167. package/src/bmm/testarch/knowledge/fixture-architecture.md +401 -0
  168. package/src/bmm/testarch/knowledge/fixtures-composition.md +382 -0
  169. package/src/bmm/testarch/knowledge/intercept-network-call.md +430 -0
  170. package/src/bmm/testarch/knowledge/log.md +429 -0
  171. package/src/bmm/testarch/knowledge/network-error-monitor.md +405 -0
  172. package/src/bmm/testarch/knowledge/network-first.md +486 -0
  173. package/src/bmm/testarch/knowledge/network-recorder.md +527 -0
  174. package/src/bmm/testarch/knowledge/nfr-criteria.md +670 -0
  175. package/src/bmm/testarch/knowledge/overview.md +286 -0
  176. package/src/bmm/testarch/knowledge/playwright-config.md +730 -0
  177. package/src/bmm/testarch/knowledge/probability-impact.md +601 -0
  178. package/src/bmm/testarch/knowledge/recurse.md +421 -0
  179. package/src/bmm/testarch/knowledge/risk-governance.md +615 -0
  180. package/src/bmm/testarch/knowledge/selective-testing.md +732 -0
  181. package/src/bmm/testarch/knowledge/selector-resilience.md +527 -0
  182. package/src/bmm/testarch/knowledge/test-healing-patterns.md +644 -0
  183. package/src/bmm/testarch/knowledge/test-levels-framework.md +473 -0
  184. package/src/bmm/testarch/knowledge/test-priorities-matrix.md +373 -0
  185. package/src/bmm/testarch/knowledge/test-quality.md +664 -0
  186. package/src/bmm/testarch/knowledge/timing-debugging.md +372 -0
  187. package/src/bmm/testarch/knowledge/visual-debugging.md +524 -0
  188. package/src/bmm/testarch/tea-index.csv +35 -0
  189. package/src/bmm/workflows/1-analysis/create-product-brief/product-brief.template.md +10 -0
  190. package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-01-init.md +177 -0
  191. package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-01b-continue.md +161 -0
  192. package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-02-vision.md +199 -0
  193. package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-03-users.md +202 -0
  194. package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-04-metrics.md +205 -0
  195. package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-05-scope.md +219 -0
  196. package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-06-complete.md +162 -0
  197. package/src/bmm/workflows/1-analysis/create-product-brief/workflow.md +58 -0
  198. package/src/bmm/workflows/1-analysis/research/domain-steps/step-01-init.md +137 -0
  199. package/src/bmm/workflows/1-analysis/research/domain-steps/step-02-domain-analysis.md +229 -0
  200. package/src/bmm/workflows/1-analysis/research/domain-steps/step-03-competitive-landscape.md +238 -0
  201. package/src/bmm/workflows/1-analysis/research/domain-steps/step-04-regulatory-focus.md +206 -0
  202. package/src/bmm/workflows/1-analysis/research/domain-steps/step-05-technical-trends.md +234 -0
  203. package/src/bmm/workflows/1-analysis/research/domain-steps/step-06-research-synthesis.md +443 -0
  204. package/src/bmm/workflows/1-analysis/research/market-steps/step-01-init.md +182 -0
  205. package/src/bmm/workflows/1-analysis/research/market-steps/step-02-customer-behavior.md +237 -0
  206. package/src/bmm/workflows/1-analysis/research/market-steps/step-02-customer-insights.md +200 -0
  207. package/src/bmm/workflows/1-analysis/research/market-steps/step-03-customer-pain-points.md +249 -0
  208. package/src/bmm/workflows/1-analysis/research/market-steps/step-04-customer-decisions.md +259 -0
  209. package/src/bmm/workflows/1-analysis/research/market-steps/step-05-competitive-analysis.md +177 -0
  210. package/src/bmm/workflows/1-analysis/research/market-steps/step-06-research-completion.md +475 -0
  211. package/src/bmm/workflows/1-analysis/research/research.template.md +29 -0
  212. package/src/bmm/workflows/1-analysis/research/technical-steps/step-01-init.md +137 -0
  213. package/src/bmm/workflows/1-analysis/research/technical-steps/step-02-technical-overview.md +239 -0
  214. package/src/bmm/workflows/1-analysis/research/technical-steps/step-03-integration-patterns.md +248 -0
  215. package/src/bmm/workflows/1-analysis/research/technical-steps/step-04-architectural-patterns.md +202 -0
  216. package/src/bmm/workflows/1-analysis/research/technical-steps/step-05-implementation-research.md +239 -0
  217. package/src/bmm/workflows/1-analysis/research/technical-steps/step-06-research-synthesis.md +486 -0
  218. package/src/bmm/workflows/1-analysis/research/workflow.md +173 -0
  219. package/src/bmm/workflows/2-plan-workflows/create-prd/data/domain-complexity.csv +13 -0
  220. package/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md +197 -0
  221. package/src/bmm/workflows/2-plan-workflows/create-prd/data/project-types.csv +11 -0
  222. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-01-init.md +191 -0
  223. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-01b-continue.md +153 -0
  224. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02-discovery.md +224 -0
  225. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-03-success.md +226 -0
  226. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-04-journeys.md +213 -0
  227. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-05-domain.md +207 -0
  228. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-06-innovation.md +226 -0
  229. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-07-project-type.md +237 -0
  230. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-08-scoping.md +228 -0
  231. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-09-functional.md +231 -0
  232. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-10-nonfunctional.md +242 -0
  233. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-11-polish.md +217 -0
  234. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-12-complete.md +124 -0
  235. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-01-discovery.md +247 -0
  236. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-01b-legacy-conversion.md +208 -0
  237. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-02-review.md +249 -0
  238. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-03-edit.md +253 -0
  239. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-04-complete.md +168 -0
  240. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-01-discovery.md +218 -0
  241. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-02-format-detection.md +191 -0
  242. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-02b-parity-check.md +209 -0
  243. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-03-density-validation.md +174 -0
  244. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-04-brief-coverage-validation.md +214 -0
  245. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-05-measurability-validation.md +228 -0
  246. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-06-traceability-validation.md +217 -0
  247. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-07-implementation-leakage-validation.md +205 -0
  248. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-08-domain-compliance-validation.md +243 -0
  249. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-09-project-type-validation.md +263 -0
  250. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-10-smart-validation.md +209 -0
  251. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-11-holistic-quality-validation.md +264 -0
  252. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-12-completeness-validation.md +242 -0
  253. package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-13-report-complete.md +231 -0
  254. package/src/bmm/workflows/2-plan-workflows/create-prd/templates/prd-template.md +10 -0
  255. package/src/bmm/workflows/2-plan-workflows/create-prd/validation-report-prd-workflow.md +433 -0
  256. package/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md +150 -0
  257. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-01-init.md +135 -0
  258. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-01b-continue.md +127 -0
  259. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-02-discovery.md +190 -0
  260. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-03-core-experience.md +216 -0
  261. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-04-emotional-response.md +219 -0
  262. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-05-inspiration.md +234 -0
  263. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-06-design-system.md +252 -0
  264. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-07-defining-experience.md +254 -0
  265. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-08-visual-foundation.md +224 -0
  266. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-09-design-directions.md +224 -0
  267. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-10-user-journeys.md +241 -0
  268. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-11-component-strategy.md +248 -0
  269. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-12-ux-patterns.md +237 -0
  270. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-13-responsive-accessibility.md +264 -0
  271. package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-14-complete.md +171 -0
  272. package/src/bmm/workflows/2-plan-workflows/create-ux-design/ux-design-template.md +13 -0
  273. package/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md +43 -0
  274. package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-01-document-discovery.md +190 -0
  275. package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-02-prd-analysis.md +178 -0
  276. package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-03-epic-coverage-validation.md +179 -0
  277. package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-04-ux-alignment.md +139 -0
  278. package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-05-epic-quality-review.md +252 -0
  279. package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-06-final-assessment.md +135 -0
  280. package/src/bmm/workflows/3-solutioning/check-implementation-readiness/templates/readiness-report-template.md +4 -0
  281. package/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md +55 -0
  282. package/src/bmm/workflows/3-solutioning/create-architecture/architecture-decision-template.md +12 -0
  283. package/src/bmm/workflows/3-solutioning/create-architecture/data/domain-complexity.csv +11 -0
  284. package/src/bmm/workflows/3-solutioning/create-architecture/data/project-types.csv +7 -0
  285. package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-01-init.md +153 -0
  286. package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-01b-continue.md +164 -0
  287. package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-02-context.md +224 -0
  288. package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-03-starter.md +331 -0
  289. package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-04-decisions.md +318 -0
  290. package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-05-patterns.md +359 -0
  291. package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-06-structure.md +379 -0
  292. package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-07-validation.md +359 -0
  293. package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-08-complete.md +76 -0
  294. package/src/bmm/workflows/3-solutioning/create-architecture/workflow.md +50 -0
  295. package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-01-validate-prerequisites.md +259 -0
  296. package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-02-design-epics.md +233 -0
  297. package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-03-create-stories.md +272 -0
  298. package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-04-final-validation.md +149 -0
  299. package/src/bmm/workflows/3-solutioning/create-epics-and-stories/templates/epics-template.md +57 -0
  300. package/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md +59 -0
  301. package/src/bmm/workflows/4-implementation/code-review/checklist.md +23 -0
  302. package/src/bmm/workflows/4-implementation/code-review/instructions.xml +227 -0
  303. package/src/bmm/workflows/4-implementation/code-review/workflow.yaml +51 -0
  304. package/src/bmm/workflows/4-implementation/correct-course/checklist.md +288 -0
  305. package/src/bmm/workflows/4-implementation/correct-course/instructions.md +206 -0
  306. package/src/bmm/workflows/4-implementation/correct-course/workflow.yaml +60 -0
  307. package/src/bmm/workflows/4-implementation/create-story/checklist.md +358 -0
  308. package/src/bmm/workflows/4-implementation/create-story/instructions.xml +345 -0
  309. package/src/bmm/workflows/4-implementation/create-story/template.md +49 -0
  310. package/src/bmm/workflows/4-implementation/create-story/workflow.yaml +61 -0
  311. package/src/bmm/workflows/4-implementation/dev-story/checklist.md +80 -0
  312. package/src/bmm/workflows/4-implementation/dev-story/instructions.xml +410 -0
  313. package/src/bmm/workflows/4-implementation/dev-story/workflow.yaml +32 -0
  314. package/src/bmm/workflows/4-implementation/fix-and-test/workflow.md +197 -0
  315. package/src/bmm/workflows/4-implementation/retrospective/instructions.md +1443 -0
  316. package/src/bmm/workflows/4-implementation/retrospective/workflow.yaml +58 -0
  317. package/src/bmm/workflows/4-implementation/sprint-planning/checklist.md +33 -0
  318. package/src/bmm/workflows/4-implementation/sprint-planning/instructions.md +225 -0
  319. package/src/bmm/workflows/4-implementation/sprint-planning/sprint-status-template.yaml +55 -0
  320. package/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml +54 -0
  321. package/src/bmm/workflows/4-implementation/sprint-status/instructions.md +229 -0
  322. package/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml +36 -0
  323. package/src/bmm/workflows/bmad-quick-flow/brownfield-fast-track/steps/step-01-detect.md +55 -0
  324. package/src/bmm/workflows/bmad-quick-flow/brownfield-fast-track/steps/step-02-confirm.md +48 -0
  325. package/src/bmm/workflows/bmad-quick-flow/brownfield-fast-track/steps/step-03-implement.md +61 -0
  326. package/src/bmm/workflows/bmad-quick-flow/brownfield-fast-track/workflow.md +41 -0
  327. package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-01-mode-detection.md +176 -0
  328. package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-02-context-gathering.md +120 -0
  329. package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-03-execute.md +153 -0
  330. package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-04-self-check.md +113 -0
  331. package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-05-adversarial-review.md +106 -0
  332. package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-06-resolve-findings.md +149 -0
  333. package/src/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md +73 -0
  334. package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-01-understand.md +192 -0
  335. package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-02-investigate.md +145 -0
  336. package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-03-generate.md +128 -0
  337. package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md +201 -0
  338. package/src/bmm/workflows/bmad-quick-flow/quick-spec/tech-spec-template.md +74 -0
  339. package/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md +93 -0
  340. package/src/bmm/workflows/bmad-quick-flow/yolo-mode/steps/step-01-rapid-spec.md +54 -0
  341. package/src/bmm/workflows/bmad-quick-flow/yolo-mode/steps/step-02-ship-it.md +65 -0
  342. package/src/bmm/workflows/bmad-quick-flow/yolo-mode/workflow.md +54 -0
  343. package/src/bmm/workflows/document-project/checklist.md +245 -0
  344. package/src/bmm/workflows/document-project/documentation-requirements.csv +12 -0
  345. package/src/bmm/workflows/document-project/instructions.md +221 -0
  346. package/src/bmm/workflows/document-project/templates/deep-dive-template.md +345 -0
  347. package/src/bmm/workflows/document-project/templates/index-template.md +169 -0
  348. package/src/bmm/workflows/document-project/templates/project-overview-template.md +103 -0
  349. package/src/bmm/workflows/document-project/templates/project-scan-report-schema.json +160 -0
  350. package/src/bmm/workflows/document-project/templates/source-tree-template.md +135 -0
  351. package/src/bmm/workflows/document-project/workflow.yaml +30 -0
  352. package/src/bmm/workflows/document-project/workflows/deep-dive-instructions.md +298 -0
  353. package/src/bmm/workflows/document-project/workflows/deep-dive.yaml +31 -0
  354. package/src/bmm/workflows/document-project/workflows/full-scan-instructions.md +1106 -0
  355. package/src/bmm/workflows/document-project/workflows/full-scan.yaml +31 -0
  356. package/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json +90 -0
  357. package/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml +127 -0
  358. package/src/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md +39 -0
  359. package/src/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md +130 -0
  360. package/src/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml +27 -0
  361. package/src/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md +43 -0
  362. package/src/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md +141 -0
  363. package/src/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml +27 -0
  364. package/src/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md +49 -0
  365. package/src/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md +241 -0
  366. package/src/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml +27 -0
  367. package/src/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md +38 -0
  368. package/src/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md +133 -0
  369. package/src/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml +27 -0
  370. package/src/bmm/workflows/parallel-work/parallel-analysis/steps/step-01-parse-target.md +49 -0
  371. package/src/bmm/workflows/parallel-work/parallel-analysis/steps/step-02-launch-streams.md +135 -0
  372. package/src/bmm/workflows/parallel-work/parallel-analysis/steps/step-03-sync-point.md +74 -0
  373. package/src/bmm/workflows/parallel-work/parallel-analysis/steps/step-04-merge-insights.md +179 -0
  374. package/src/bmm/workflows/parallel-work/parallel-analysis/workflow.md +55 -0
  375. package/src/bmm/workflows/parallel-work/parallel-generation/workflow.md +109 -0
  376. package/src/bmm/workflows/parallel-work/parallel-reviews/workflow.md +111 -0
  377. package/src/bmm/workflows/parallel-work/parallel-stories/workflow.md +112 -0
  378. package/src/bmm/workflows/parallel-work/sprint-mode/steps/step-01-load-sprint.md +54 -0
  379. package/src/bmm/workflows/parallel-work/sprint-mode/steps/step-02-plan-execution.md +63 -0
  380. package/src/bmm/workflows/parallel-work/sprint-mode/steps/step-03-execute-stories.md +112 -0
  381. package/src/bmm/workflows/parallel-work/sprint-mode/steps/step-04-code-review.md +148 -0
  382. package/src/bmm/workflows/parallel-work/sprint-mode/steps/step-05-integration-testing.md +200 -0
  383. package/src/bmm/workflows/parallel-work/sprint-mode/steps/step-06-sprint-report.md +290 -0
  384. package/src/bmm/workflows/parallel-work/sprint-mode/workflow.md +58 -0
  385. package/src/bmm/workflows/testarch/atdd/atdd-checklist-template.md +363 -0
  386. package/src/bmm/workflows/testarch/atdd/checklist.md +374 -0
  387. package/src/bmm/workflows/testarch/atdd/instructions.md +806 -0
  388. package/src/bmm/workflows/testarch/atdd/workflow.yaml +47 -0
  389. package/src/bmm/workflows/testarch/automate/checklist.md +582 -0
  390. package/src/bmm/workflows/testarch/automate/instructions.md +1324 -0
  391. package/src/bmm/workflows/testarch/automate/workflow.yaml +54 -0
  392. package/src/bmm/workflows/testarch/ci/checklist.md +247 -0
  393. package/src/bmm/workflows/testarch/ci/github-actions-template.yaml +198 -0
  394. package/src/bmm/workflows/testarch/ci/gitlab-ci-template.yaml +149 -0
  395. package/src/bmm/workflows/testarch/ci/instructions.md +536 -0
  396. package/src/bmm/workflows/testarch/ci/workflow.yaml +47 -0
  397. package/src/bmm/workflows/testarch/framework/checklist.md +320 -0
  398. package/src/bmm/workflows/testarch/framework/instructions.md +481 -0
  399. package/src/bmm/workflows/testarch/framework/workflow.yaml +49 -0
  400. package/src/bmm/workflows/testarch/nfr-assess/checklist.md +407 -0
  401. package/src/bmm/workflows/testarch/nfr-assess/instructions.md +726 -0
  402. package/src/bmm/workflows/testarch/nfr-assess/nfr-report-template.md +461 -0
  403. package/src/bmm/workflows/testarch/nfr-assess/workflow.yaml +49 -0
  404. package/src/bmm/workflows/testarch/test-design/checklist.md +407 -0
  405. package/src/bmm/workflows/testarch/test-design/instructions.md +1158 -0
  406. package/src/bmm/workflows/testarch/test-design/test-design-architecture-template.md +213 -0
  407. package/src/bmm/workflows/testarch/test-design/test-design-qa-template.md +286 -0
  408. package/src/bmm/workflows/testarch/test-design/test-design-template.md +294 -0
  409. package/src/bmm/workflows/testarch/test-design/workflow.yaml +71 -0
  410. package/src/bmm/workflows/testarch/test-review/checklist.md +472 -0
  411. package/src/bmm/workflows/testarch/test-review/instructions.md +628 -0
  412. package/src/bmm/workflows/testarch/test-review/test-review-template.md +390 -0
  413. package/src/bmm/workflows/testarch/test-review/workflow.yaml +48 -0
  414. package/src/bmm/workflows/testarch/trace/checklist.md +642 -0
  415. package/src/bmm/workflows/testarch/trace/instructions.md +1030 -0
  416. package/src/bmm/workflows/testarch/trace/trace-template.md +675 -0
  417. package/src/bmm/workflows/testarch/trace/workflow.yaml +57 -0
  418. package/src/core/_module-installer/installer.js +60 -0
  419. package/src/core/agents/bmad-master.agent.yaml +29 -0
  420. package/src/core/module-help.csv +9 -0
  421. package/src/core/module.yaml +25 -0
  422. package/src/core/resources/excalidraw/README.md +160 -0
  423. package/src/core/resources/excalidraw/excalidraw-helpers.md +127 -0
  424. package/src/core/resources/excalidraw/library-loader.md +50 -0
  425. package/src/core/resources/excalidraw/validate-json-instructions.md +79 -0
  426. package/src/core/tasks/editorial-review-prose.xml +100 -0
  427. package/src/core/tasks/editorial-review-structure.xml +209 -0
  428. package/src/core/tasks/help.md +62 -0
  429. package/src/core/tasks/index-docs.xml +65 -0
  430. package/src/core/tasks/review-adversarial-general.xml +48 -0
  431. package/src/core/tasks/shard-doc.xml +109 -0
  432. package/src/core/tasks/workflow.xml +235 -0
  433. package/src/core/workflows/advanced-elicitation/methods.csv +51 -0
  434. package/src/core/workflows/advanced-elicitation/workflow.xml +117 -0
  435. package/src/core/workflows/brainstorming/brain-methods.csv +62 -0
  436. package/src/core/workflows/brainstorming/steps/step-01-session-setup.md +197 -0
  437. package/src/core/workflows/brainstorming/steps/step-01b-continue.md +122 -0
  438. package/src/core/workflows/brainstorming/steps/step-02a-user-selected.md +225 -0
  439. package/src/core/workflows/brainstorming/steps/step-02b-ai-recommended.md +237 -0
  440. package/src/core/workflows/brainstorming/steps/step-02c-random-selection.md +209 -0
  441. package/src/core/workflows/brainstorming/steps/step-02d-progressive-flow.md +264 -0
  442. package/src/core/workflows/brainstorming/steps/step-03-technique-execution.md +399 -0
  443. package/src/core/workflows/brainstorming/steps/step-04-idea-organization.md +303 -0
  444. package/src/core/workflows/brainstorming/template.md +15 -0
  445. package/src/core/workflows/brainstorming/workflow.md +58 -0
  446. package/src/core/workflows/party-mode/steps/step-01-agent-loading.md +138 -0
  447. package/src/core/workflows/party-mode/steps/step-02-discussion-orchestration.md +187 -0
  448. package/src/core/workflows/party-mode/steps/step-03-graceful-exit.md +157 -0
  449. package/src/core/workflows/party-mode/workflow.md +194 -0
  450. package/src/utility/agent-components/activation-rules.txt +6 -0
  451. package/src/utility/agent-components/activation-steps.txt +14 -0
  452. package/src/utility/agent-components/agent-command-header.md +1 -0
  453. package/src/utility/agent-components/agent.customize.template.yaml +41 -0
  454. package/src/utility/agent-components/handler-action.txt +4 -0
  455. package/src/utility/agent-components/handler-data.txt +5 -0
  456. package/src/utility/agent-components/handler-exec.txt +6 -0
  457. package/src/utility/agent-components/handler-multi.txt +14 -0
  458. package/src/utility/agent-components/handler-tmpl.txt +5 -0
  459. package/src/utility/agent-components/handler-validate-workflow.txt +7 -0
  460. package/src/utility/agent-components/handler-workflow.txt +10 -0
  461. package/src/utility/agent-components/menu-handlers.txt +6 -0
  462. package/tools/bmad-npx-wrapper.js +69 -0
  463. package/tools/build-docs.js +577 -0
  464. package/tools/cli/README.md +7 -0
  465. package/tools/cli/bmad-cli.js +65 -0
  466. package/tools/cli/commands/diagnostics.js +303 -0
  467. package/tools/cli/commands/install.js +87 -0
  468. package/tools/cli/commands/module.js +210 -0
  469. package/tools/cli/commands/status.js +65 -0
  470. package/tools/cli/commands/uninstall.js +86 -0
  471. package/tools/cli/external-official-modules.yaml +54 -0
  472. package/tools/cli/installers/install-messages.yaml +59 -0
  473. package/tools/cli/installers/lib/core/config-collector.js +1079 -0
  474. package/tools/cli/installers/lib/core/custom-module-cache.js +259 -0
  475. package/tools/cli/installers/lib/core/dependency-resolver.js +739 -0
  476. package/tools/cli/installers/lib/core/detector.js +223 -0
  477. package/tools/cli/installers/lib/core/ide-config-manager.js +156 -0
  478. package/tools/cli/installers/lib/core/installer.js +2812 -0
  479. package/tools/cli/installers/lib/core/manifest-generator.js +1054 -0
  480. package/tools/cli/installers/lib/core/manifest.js +1036 -0
  481. package/tools/cli/installers/lib/custom/handler.js +363 -0
  482. package/tools/cli/installers/lib/ide/_base-ide.js +655 -0
  483. package/tools/cli/installers/lib/ide/_config-driven.js +450 -0
  484. package/tools/cli/installers/lib/ide/codex.js +440 -0
  485. package/tools/cli/installers/lib/ide/kilo.js +250 -0
  486. package/tools/cli/installers/lib/ide/kiro-cli.js +326 -0
  487. package/tools/cli/installers/lib/ide/manager.js +271 -0
  488. package/tools/cli/installers/lib/ide/platform-codes.js +100 -0
  489. package/tools/cli/installers/lib/ide/platform-codes.yaml +227 -0
  490. package/tools/cli/installers/lib/ide/shared/agent-command-generator.js +181 -0
  491. package/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +163 -0
  492. package/tools/cli/installers/lib/ide/shared/module-injections.js +136 -0
  493. package/tools/cli/installers/lib/ide/shared/path-utils.js +292 -0
  494. package/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +270 -0
  495. package/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +319 -0
  496. package/tools/cli/installers/lib/ide/templates/agent-command-template.md +14 -0
  497. package/tools/cli/installers/lib/ide/templates/combined/antigravity.md +8 -0
  498. package/tools/cli/installers/lib/ide/templates/combined/default-agent.md +15 -0
  499. package/tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md +14 -0
  500. package/tools/cli/installers/lib/ide/templates/combined/default-workflow.md +6 -0
  501. package/tools/cli/installers/lib/ide/templates/combined/gemini-agent.toml +14 -0
  502. package/tools/cli/installers/lib/ide/templates/combined/gemini-workflow-yaml.toml +16 -0
  503. package/tools/cli/installers/lib/ide/templates/combined/gemini-workflow.toml +14 -0
  504. package/tools/cli/installers/lib/ide/templates/combined/rovodev.md +9 -0
  505. package/tools/cli/installers/lib/ide/templates/combined/trae.md +9 -0
  506. package/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md +10 -0
  507. package/tools/cli/installers/lib/ide/templates/split/opencode/body.md +10 -0
  508. package/tools/cli/installers/lib/ide/templates/split/opencode/header.md +4 -0
  509. package/tools/cli/installers/lib/ide/templates/workflow-command-template.md +13 -0
  510. package/tools/cli/installers/lib/ide/templates/workflow-commander.md +5 -0
  511. package/tools/cli/installers/lib/message-loader.js +85 -0
  512. package/tools/cli/installers/lib/modules/external-manager.js +135 -0
  513. package/tools/cli/installers/lib/modules/manager.js +1375 -0
  514. package/tools/cli/lib/activation-builder.js +163 -0
  515. package/tools/cli/lib/agent/compiler.js +522 -0
  516. package/tools/cli/lib/agent/compiler.ts +572 -0
  517. package/tools/cli/lib/agent/installer.js +716 -0
  518. package/tools/cli/lib/agent/template-engine.js +152 -0
  519. package/tools/cli/lib/agent/types.ts +155 -0
  520. package/tools/cli/lib/agent-analyzer.js +109 -0
  521. package/tools/cli/lib/agent-party-generator.js +194 -0
  522. package/tools/cli/lib/cli-utils.js +227 -0
  523. package/tools/cli/lib/config.js +213 -0
  524. package/tools/cli/lib/config.ts +227 -0
  525. package/tools/cli/lib/file-ops.js +204 -0
  526. package/tools/cli/lib/file-ops.ts +215 -0
  527. package/tools/cli/lib/platform-codes.js +116 -0
  528. package/tools/cli/lib/project-root.js +77 -0
  529. package/tools/cli/lib/prompts.js +433 -0
  530. package/tools/cli/lib/prompts.ts +541 -0
  531. package/tools/cli/lib/types/config.types.ts +43 -0
  532. package/tools/cli/lib/types/xml-handler.types.ts +50 -0
  533. package/tools/cli/lib/ui.js +1660 -0
  534. package/tools/cli/lib/xml-handler.js +177 -0
  535. package/tools/cli/lib/xml-handler.ts +188 -0
  536. package/tools/cli/lib/xml-to-markdown.js +82 -0
  537. package/tools/cli/lib/yaml-format.js +245 -0
  538. package/tools/cli/lib/yaml-xml-builder.js +587 -0
  539. package/tools/docs/BUNDLE_DISTRIBUTION_SETUP.md +95 -0
  540. package/tools/docs/fix-refs.md +91 -0
  541. package/tools/docs/index.md +2 -0
  542. package/tools/fix-doc-links.js +288 -0
  543. package/tools/flattener/aggregate.js +76 -0
  544. package/tools/flattener/aggregate.ts +78 -0
  545. package/tools/flattener/binary.js +80 -0
  546. package/tools/flattener/discovery.js +71 -0
  547. package/tools/flattener/files.js +35 -0
  548. package/tools/flattener/files.ts +31 -0
  549. package/tools/flattener/ignoreRules.js +172 -0
  550. package/tools/flattener/main.js +483 -0
  551. package/tools/flattener/main.ts +262 -0
  552. package/tools/flattener/projectRoot.js +201 -0
  553. package/tools/flattener/prompts.js +44 -0
  554. package/tools/flattener/stats.helpers.js +368 -0
  555. package/tools/flattener/stats.js +75 -0
  556. package/tools/flattener/test-matrix.js +409 -0
  557. package/tools/flattener/types.ts +53 -0
  558. package/tools/flattener/xml.js +82 -0
  559. package/tools/format-workflow-md.js +263 -0
  560. package/tools/lib/xml-utils.js +13 -0
  561. package/tools/maintainer/review-pr-README.md +55 -0
  562. package/tools/maintainer/review-pr.md +242 -0
  563. package/tools/migrate-custom-module-paths.js +124 -0
  564. package/tools/platform-codes.yaml +157 -0
  565. package/tools/schema/agent.js +491 -0
  566. package/tools/schema/agent.ts +489 -0
  567. package/tools/schema/agent.types.ts +31 -0
  568. package/tools/update-bmad.sh +24 -0
  569. package/tools/validate-agent-schema.js +110 -0
  570. package/tools/validate-doc-links.js +371 -0
  571. package/tools/validate-svg-changes.sh +356 -0
  572. package/vite-plugin-monaco-editor.ts +108 -0
@@ -0,0 +1,2812 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const { Detector } = require('./detector');
6
+ const { Manifest } = require('./manifest');
7
+ const { ModuleManager } = require('../modules/manager');
8
+ const { IdeManager } = require('../ide/manager');
9
+ const { FileOps } = require('../../../lib/file-ops');
10
+ const { Config } = require('../../../lib/config');
11
+ const { XmlHandler } = require('../../../lib/xml-handler');
12
+ const { DependencyResolver } = require('./dependency-resolver');
13
+ const { ConfigCollector } = require('./config-collector');
14
+ const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
15
+ const { CLIUtils } = require('../../../lib/cli-utils');
16
+ const { ManifestGenerator } = require('./manifest-generator');
17
+ const { IdeConfigManager } = require('./ide-config-manager');
18
+ const { CustomHandler } = require('../custom/handler');
19
+ const prompts = require('../../../lib/prompts');
20
+
21
+ // BMAD installation folder name - this is constant and should never change
22
+ const BMAD_FOLDER_NAME = '_bmad';
23
+
24
+ class Installer {
25
+ constructor() {
26
+ this.detector = new Detector();
27
+ this.manifest = new Manifest();
28
+ this.moduleManager = new ModuleManager();
29
+ this.ideManager = new IdeManager();
30
+ this.fileOps = new FileOps();
31
+ this.config = new Config();
32
+ this.xmlHandler = new XmlHandler();
33
+ this.dependencyResolver = new DependencyResolver();
34
+ this.configCollector = new ConfigCollector();
35
+ this.ideConfigManager = new IdeConfigManager();
36
+ this.installedFiles = new Set(); // Track all installed files
37
+ this.bmadFolderName = BMAD_FOLDER_NAME;
38
+ }
39
+
40
+ /**
41
+ * Find the bmad installation directory in a project
42
+ * Always uses the standard _bmad folder name
43
+ * Also checks for legacy _cfg folder for migration
44
+ * @param {string} projectDir - Project directory
45
+ * @returns {Promise<Object>} { bmadDir: string, hasLegacyCfg: boolean }
46
+ */
47
+ async findBmadDir(projectDir) {
48
+ const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
49
+
50
+ // Check if project directory exists
51
+ if (!(await fs.pathExists(projectDir))) {
52
+ // Project doesn't exist yet, return default
53
+ return { bmadDir, hasLegacyCfg: false };
54
+ }
55
+
56
+ // Check for legacy _cfg folder if bmad directory exists
57
+ let hasLegacyCfg = false;
58
+ if (await fs.pathExists(bmadDir)) {
59
+ const legacyCfgPath = path.join(bmadDir, '_cfg');
60
+ if (await fs.pathExists(legacyCfgPath)) {
61
+ hasLegacyCfg = true;
62
+ }
63
+ }
64
+
65
+ return { bmadDir, hasLegacyCfg };
66
+ }
67
+
68
+ /**
69
+ * @function copyFileWithPlaceholderReplacement
70
+ * @intent Copy files from BMAD source to installation directory with dynamic content transformation
71
+ * @why Enables installation-time customization: _bmad replacement
72
+ * @param {string} sourcePath - Absolute path to source file in BMAD repository
73
+ * @param {string} targetPath - Absolute path to destination file in user's project
74
+ * @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
75
+ * @returns {Promise<void>} Resolves when file copy and transformation complete
76
+ * @sideeffects Writes transformed file to targetPath, creates parent directories if needed
77
+ * @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails
78
+ * @calledby installCore(), installModule(), IDE installers during file vendoring
79
+ * @calls fs.readFile(), fs.writeFile(), fs.copy()
80
+ *
81
+
82
+ *
83
+ * 3. Document marker in instructions.md (if applicable)
84
+ */
85
+ async copyFileWithPlaceholderReplacement(sourcePath, targetPath) {
86
+ // List of text file extensions that should have placeholder replacement
87
+ const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
88
+ const ext = path.extname(sourcePath).toLowerCase();
89
+
90
+ // Check if this is a text file that might contain placeholders
91
+ if (textExtensions.includes(ext)) {
92
+ try {
93
+ // Read the file content
94
+ let content = await fs.readFile(sourcePath, 'utf8');
95
+
96
+ // Write to target with replaced content
97
+ await fs.ensureDir(path.dirname(targetPath));
98
+ await fs.writeFile(targetPath, content, 'utf8');
99
+ } catch {
100
+ // If reading as text fails (might be binary despite extension), fall back to regular copy
101
+ await fs.copy(sourcePath, targetPath, { overwrite: true });
102
+ }
103
+ } else {
104
+ // Binary file or other file type - just copy directly
105
+ await fs.copy(sourcePath, targetPath, { overwrite: true });
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Collect Tool/IDE configurations after module configuration
111
+ * @param {string} projectDir - Project directory
112
+ * @param {Array} selectedModules - Selected modules from configuration
113
+ * @param {boolean} isFullReinstall - Whether this is a full reinstall
114
+ * @param {Array} previousIdes - Previously configured IDEs (for reinstalls)
115
+ * @param {Array} preSelectedIdes - Pre-selected IDEs from early prompt (optional)
116
+ * @returns {Object} Tool/IDE selection and configurations
117
+ */
118
+ async collectToolConfigurations(projectDir, selectedModules, isFullReinstall = false, previousIdes = [], preSelectedIdes = null) {
119
+ // Use pre-selected IDEs if provided, otherwise prompt
120
+ let toolConfig;
121
+ if (preSelectedIdes === null) {
122
+ // Fallback: prompt for tool selection (backwards compatibility)
123
+ const { UI } = require('../../../lib/ui');
124
+ const ui = new UI();
125
+ toolConfig = await ui.promptToolSelection(projectDir);
126
+ } else {
127
+ // IDEs were already selected during initial prompts
128
+ toolConfig = {
129
+ ides: preSelectedIdes,
130
+ skipIde: !preSelectedIdes || preSelectedIdes.length === 0,
131
+ };
132
+ }
133
+
134
+ // Check for already configured IDEs
135
+ const { Detector } = require('./detector');
136
+ const detector = new Detector();
137
+ const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
138
+
139
+ // During full reinstall, use the saved previous IDEs since bmad dir was deleted
140
+ // Otherwise detect from existing installation
141
+ let previouslyConfiguredIdes;
142
+ if (isFullReinstall) {
143
+ // During reinstall, treat all IDEs as new (need configuration)
144
+ previouslyConfiguredIdes = [];
145
+ } else {
146
+ const existingInstall = await detector.detect(bmadDir);
147
+ previouslyConfiguredIdes = existingInstall.ides || [];
148
+ }
149
+
150
+ // Load saved IDE configurations for already-configured IDEs
151
+ const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
152
+
153
+ // Collect IDE-specific configurations if any were selected
154
+ const ideConfigurations = {};
155
+
156
+ // First, add saved configs for already-configured IDEs
157
+ for (const ide of toolConfig.ides || []) {
158
+ if (previouslyConfiguredIdes.includes(ide) && savedIdeConfigs[ide]) {
159
+ ideConfigurations[ide] = savedIdeConfigs[ide];
160
+ }
161
+ }
162
+
163
+ if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
164
+ // Ensure IDE manager is initialized
165
+ await this.ideManager.ensureInitialized();
166
+
167
+ // Determine which IDEs are newly selected (not previously configured)
168
+ const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide));
169
+
170
+ if (newlySelectedIdes.length > 0) {
171
+ console.log('\n'); // Add spacing before IDE questions
172
+
173
+ // Collect configuration for IDEs that support it
174
+ for (const ide of newlySelectedIdes) {
175
+ try {
176
+ const handler = this.ideManager.handlers.get(ide);
177
+
178
+ if (!handler) {
179
+ console.warn(chalk.yellow(`Warning: IDE '${ide}' handler not found`));
180
+ continue;
181
+ }
182
+
183
+ // Check if this IDE handler has a collectConfiguration method
184
+ // (custom installers like Codex, Kilo, Kiro-cli may have this)
185
+ if (typeof handler.collectConfiguration === 'function') {
186
+ console.log(chalk.cyan(`\nConfiguring ${ide}...`));
187
+ ideConfigurations[ide] = await handler.collectConfiguration({
188
+ selectedModules: selectedModules || [],
189
+ projectDir,
190
+ bmadDir,
191
+ });
192
+ }
193
+ // Most config-driven IDEs don't need configuration - silently skip
194
+ } catch (error) {
195
+ // IDE doesn't support configuration or has an error
196
+ console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}: ${error.message}`));
197
+ }
198
+ }
199
+ }
200
+
201
+ // Log which IDEs are already configured and being kept
202
+ const keptIdes = toolConfig.ides.filter((ide) => previouslyConfiguredIdes.includes(ide));
203
+ if (keptIdes.length > 0) {
204
+ console.log(chalk.dim(`\nKeeping existing configuration for: ${keptIdes.join(', ')}`));
205
+ }
206
+ }
207
+
208
+ return {
209
+ ides: toolConfig.ides,
210
+ skipIde: toolConfig.skipIde,
211
+ configurations: ideConfigurations,
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Main installation method
217
+ * @param {Object} config - Installation configuration
218
+ * @param {string} config.directory - Target directory
219
+ * @param {boolean} config.installCore - Whether to install core
220
+ * @param {string[]} config.modules - Modules to install
221
+ * @param {string[]} config.ides - IDEs to configure
222
+ * @param {boolean} config.skipIde - Skip IDE configuration
223
+ */
224
+ async install(originalConfig) {
225
+ // Clone config to avoid mutating the caller's object
226
+ const config = { ...originalConfig };
227
+
228
+ // Check if core config was already collected in UI
229
+ const hasCoreConfig = config.coreConfig && Object.keys(config.coreConfig).length > 0;
230
+
231
+ // Only display logo if core config wasn't already collected (meaning we're not continuing from UI)
232
+ if (!hasCoreConfig) {
233
+ // Display BMAD logo
234
+ CLIUtils.displayLogo();
235
+
236
+ // Display welcome message
237
+ CLIUtils.displaySection('BMad™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version);
238
+ }
239
+
240
+ // Note: Legacy V4 detection now happens earlier in UI.promptInstall()
241
+ // before any config collection, so we don't need to check again here
242
+
243
+ const projectDir = path.resolve(config.directory);
244
+
245
+ // If core config was pre-collected (from interactive mode), use it
246
+ if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
247
+ this.configCollector.collectedConfig.core = config.coreConfig;
248
+ // Also store in allAnswers for cross-referencing
249
+ this.configCollector.allAnswers = {};
250
+ for (const [key, value] of Object.entries(config.coreConfig)) {
251
+ this.configCollector.allAnswers[`core_${key}`] = value;
252
+ }
253
+ }
254
+
255
+ // Collect configurations for modules (skip if quick update already collected them)
256
+ let moduleConfigs;
257
+ let customModulePaths = new Map();
258
+
259
+ if (config._quickUpdate) {
260
+ // Quick update already collected all configs, use them directly
261
+ moduleConfigs = this.configCollector.collectedConfig;
262
+
263
+ // For quick update, populate customModulePaths from _customModuleSources
264
+ if (config._customModuleSources) {
265
+ for (const [moduleId, customInfo] of config._customModuleSources) {
266
+ customModulePaths.set(moduleId, customInfo.sourcePath);
267
+ }
268
+ }
269
+ } else {
270
+ // For regular updates (modify flow), check manifest for custom module sources
271
+ if (config._isUpdate && config._existingInstall && config._existingInstall.customModules) {
272
+ for (const customModule of config._existingInstall.customModules) {
273
+ // Ensure we have an absolute sourcePath
274
+ let absoluteSourcePath = customModule.sourcePath;
275
+
276
+ // Check if sourcePath is a cache-relative path (starts with _config)
277
+ if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
278
+ // Convert cache-relative path to absolute path
279
+ absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
280
+ }
281
+ // If no sourcePath but we have relativePath, convert it
282
+ else if (!absoluteSourcePath && customModule.relativePath) {
283
+ // relativePath is relative to the project root (parent of bmad dir)
284
+ absoluteSourcePath = path.resolve(projectDir, customModule.relativePath);
285
+ }
286
+ // Ensure sourcePath is absolute for anything else
287
+ else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
288
+ absoluteSourcePath = path.resolve(absoluteSourcePath);
289
+ }
290
+
291
+ if (absoluteSourcePath) {
292
+ customModulePaths.set(customModule.id, absoluteSourcePath);
293
+ }
294
+ }
295
+ }
296
+
297
+ // Build custom module paths map from customContent
298
+
299
+ // Handle selectedFiles (from existing install path or manual directory input)
300
+ if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
301
+ const customHandler = new CustomHandler();
302
+ for (const customFile of config.customContent.selectedFiles) {
303
+ const customInfo = await customHandler.getCustomInfo(customFile, path.resolve(config.directory));
304
+ if (customInfo && customInfo.id) {
305
+ customModulePaths.set(customInfo.id, customInfo.path);
306
+ }
307
+ }
308
+ }
309
+
310
+ // Handle new custom content sources from UI
311
+ if (config.customContent && config.customContent.sources) {
312
+ for (const source of config.customContent.sources) {
313
+ customModulePaths.set(source.id, source.path);
314
+ }
315
+ }
316
+
317
+ // Handle cachedModules (from new install path where modules are cached)
318
+ // Only include modules that were actually selected for installation
319
+ if (config.customContent && config.customContent.cachedModules) {
320
+ // Get selected cached module IDs (if available)
321
+ const selectedCachedIds = config.customContent.selectedCachedModules || [];
322
+ // If no selection info, include all cached modules (for backward compatibility)
323
+ const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected;
324
+
325
+ for (const cachedModule of config.customContent.cachedModules) {
326
+ // For cached modules, the path is the cachePath which contains the module.yaml
327
+ if (
328
+ cachedModule.id &&
329
+ cachedModule.cachePath && // Include if selected or if we should include all
330
+ (shouldIncludeAll || selectedCachedIds.includes(cachedModule.id))
331
+ ) {
332
+ customModulePaths.set(cachedModule.id, cachedModule.cachePath);
333
+ }
334
+ }
335
+ }
336
+
337
+ // Get list of all modules including custom modules
338
+ // Order: core first, then official modules, then custom modules
339
+ const allModulesForConfig = ['core'];
340
+
341
+ // Add official modules (excluding core and any custom modules)
342
+ const officialModules = (config.modules || []).filter((m) => m !== 'core' && !customModulePaths.has(m));
343
+ allModulesForConfig.push(...officialModules);
344
+
345
+ // Add custom modules at the end
346
+ for (const [moduleId] of customModulePaths) {
347
+ if (!allModulesForConfig.includes(moduleId)) {
348
+ allModulesForConfig.push(moduleId);
349
+ }
350
+ }
351
+
352
+ // Check if core was already collected in UI
353
+ if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
354
+ // Core already collected, skip it in config collection
355
+ const modulesWithoutCore = allModulesForConfig.filter((m) => m !== 'core');
356
+ moduleConfigs = await this.configCollector.collectAllConfigurations(modulesWithoutCore, path.resolve(config.directory), {
357
+ customModulePaths,
358
+ });
359
+ } else {
360
+ // Core not collected yet, include it
361
+ moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), {
362
+ customModulePaths,
363
+ });
364
+ }
365
+ }
366
+
367
+ // Set bmad folder name on module manager and IDE manager for placeholder replacement
368
+ this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME);
369
+ this.moduleManager.setCoreConfig(moduleConfigs.core || {});
370
+ this.moduleManager.setCustomModulePaths(customModulePaths);
371
+ this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME);
372
+
373
+ // Tool selection will be collected after we determine if it's a reinstall/update/new install
374
+
375
+ const spinner = ora('Preparing installation...').start();
376
+
377
+ try {
378
+ // Resolve target directory (path.resolve handles platform differences)
379
+ const projectDir = path.resolve(config.directory);
380
+
381
+ // Always use the standard _bmad folder name
382
+ const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
383
+
384
+ // Create a project directory if it doesn't exist (user already confirmed)
385
+ if (!(await fs.pathExists(projectDir))) {
386
+ spinner.text = 'Creating installation directory...';
387
+ try {
388
+ // fs.ensureDir handles platform-specific directory creation
389
+ // It will recursively create all necessary parent directories
390
+ await fs.ensureDir(projectDir);
391
+ } catch (error) {
392
+ spinner.fail('Failed to create installation directory');
393
+ console.error(chalk.red(`Error: ${error.message}`));
394
+ // More detailed error for common issues
395
+ if (error.code === 'EACCES') {
396
+ console.error(chalk.red('Permission denied. Check parent directory permissions.'));
397
+ } else if (error.code === 'ENOSPC') {
398
+ console.error(chalk.red('No space left on device.'));
399
+ }
400
+ throw new Error(`Cannot create directory: ${projectDir}`);
401
+ }
402
+ }
403
+
404
+ // Check existing installation
405
+ spinner.text = 'Checking for existing installation...';
406
+ const existingInstall = await this.detector.detect(bmadDir);
407
+
408
+ if (existingInstall.installed && !config.force && !config._quickUpdate) {
409
+ spinner.stop();
410
+
411
+ // Check if user already decided what to do (from early menu in ui.js)
412
+ let action = null;
413
+ if (config.actionType === 'update') {
414
+ action = 'update';
415
+ } else {
416
+ // Fallback: Ask the user (backwards compatibility for other code paths)
417
+ console.log(chalk.yellow('\n⚠️ Existing BMAD installation detected'));
418
+ console.log(chalk.dim(` Location: ${bmadDir}`));
419
+ console.log(chalk.dim(` Version: ${existingInstall.version}`));
420
+
421
+ const promptResult = await this.promptUpdateAction();
422
+ action = promptResult.action;
423
+ }
424
+
425
+ if (action === 'update') {
426
+ // Store that we're updating for later processing
427
+ config._isUpdate = true;
428
+ config._existingInstall = existingInstall;
429
+
430
+ // Detect modules that were previously installed but are NOT in the new selection (to be removed)
431
+ const previouslyInstalledModules = new Set(existingInstall.modules.map((m) => m.id));
432
+ const newlySelectedModules = new Set(config.modules || []);
433
+
434
+ // Find modules to remove (installed but not in new selection)
435
+ // Exclude 'core' from being removable
436
+ const modulesToRemove = [...previouslyInstalledModules].filter((m) => !newlySelectedModules.has(m) && m !== 'core');
437
+
438
+ // If there are modules to remove, ask for confirmation
439
+ if (modulesToRemove.length > 0) {
440
+ const prompts = require('../../../lib/prompts');
441
+ spinner.stop();
442
+
443
+ console.log('');
444
+ console.log(chalk.yellow.bold('⚠️ Modules to be removed:'));
445
+ for (const moduleId of modulesToRemove) {
446
+ const moduleInfo = existingInstall.modules.find((m) => m.id === moduleId);
447
+ const displayName = moduleInfo?.name || moduleId;
448
+ const modulePath = path.join(bmadDir, moduleId);
449
+ console.log(chalk.red(` - ${displayName} (${modulePath})`));
450
+ }
451
+ console.log('');
452
+
453
+ const confirmRemoval = await prompts.confirm({
454
+ message: `Remove ${modulesToRemove.length} module(s) from BMAD installation?`,
455
+ default: false,
456
+ });
457
+
458
+ if (confirmRemoval) {
459
+ // Remove module folders
460
+ for (const moduleId of modulesToRemove) {
461
+ const modulePath = path.join(bmadDir, moduleId);
462
+ try {
463
+ if (await fs.pathExists(modulePath)) {
464
+ await fs.remove(modulePath);
465
+ console.log(chalk.dim(` ✓ Removed: ${moduleId}`));
466
+ }
467
+ } catch (error) {
468
+ console.warn(chalk.yellow(` Warning: Failed to remove ${moduleId}: ${error.message}`));
469
+ }
470
+ }
471
+ console.log(chalk.green(` ✓ Removed ${modulesToRemove.length} module(s)`));
472
+ } else {
473
+ console.log(chalk.dim(' → Module removal cancelled'));
474
+ // Add the modules back to the selection since user cancelled removal
475
+ for (const moduleId of modulesToRemove) {
476
+ if (!config.modules) config.modules = [];
477
+ config.modules.push(moduleId);
478
+ }
479
+ }
480
+
481
+ spinner.start('Preparing update...');
482
+ }
483
+
484
+ // Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
485
+ const existingFilesManifest = await this.readFilesManifest(bmadDir);
486
+ const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
487
+
488
+ config._customFiles = customFiles;
489
+ config._modifiedFiles = modifiedFiles;
490
+
491
+ // Preserve existing core configuration during updates
492
+ // Read the current core config.yaml to maintain user's settings
493
+ const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
494
+ if ((await fs.pathExists(coreConfigPath)) && (!config.coreConfig || Object.keys(config.coreConfig).length === 0)) {
495
+ try {
496
+ const yaml = require('yaml');
497
+ const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
498
+ const existingCoreConfig = yaml.parse(coreConfigContent);
499
+
500
+ // Store in config.coreConfig so it's preserved through the installation
501
+ config.coreConfig = existingCoreConfig;
502
+
503
+ // Also store in configCollector for use during config collection
504
+ this.configCollector.collectedConfig.core = existingCoreConfig;
505
+ } catch (error) {
506
+ console.warn(chalk.yellow(`Warning: Could not read existing core config: ${error.message}`));
507
+ }
508
+ }
509
+
510
+ // Also check cache directory for custom modules (like quick update does)
511
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
512
+ if (await fs.pathExists(cacheDir)) {
513
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
514
+
515
+ for (const cachedModule of cachedModules) {
516
+ if (cachedModule.isDirectory()) {
517
+ const moduleId = cachedModule.name;
518
+
519
+ // Skip if we already have this module from manifest
520
+ if (customModulePaths.has(moduleId)) {
521
+ continue;
522
+ }
523
+
524
+ // Check if this is an external official module - skip cache for those
525
+ const isExternal = await this.moduleManager.isExternalModule(moduleId);
526
+ if (isExternal) {
527
+ // External modules are handled via cloneExternalModule, not from cache
528
+ continue;
529
+ }
530
+
531
+ const cachedPath = path.join(cacheDir, moduleId);
532
+
533
+ // Check if this is actually a custom module (has module.yaml)
534
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
535
+ if (await fs.pathExists(moduleYamlPath)) {
536
+ customModulePaths.set(moduleId, cachedPath);
537
+ }
538
+ }
539
+ }
540
+
541
+ // Update module manager with the new custom module paths from cache
542
+ this.moduleManager.setCustomModulePaths(customModulePaths);
543
+ }
544
+
545
+ // If there are custom files, back them up temporarily
546
+ if (customFiles.length > 0) {
547
+ const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
548
+ await fs.ensureDir(tempBackupDir);
549
+
550
+ spinner.start(`Backing up ${customFiles.length} custom files...`);
551
+ for (const customFile of customFiles) {
552
+ const relativePath = path.relative(bmadDir, customFile);
553
+ const backupPath = path.join(tempBackupDir, relativePath);
554
+ await fs.ensureDir(path.dirname(backupPath));
555
+ await fs.copy(customFile, backupPath);
556
+ }
557
+ spinner.succeed(`Backed up ${customFiles.length} custom files`);
558
+
559
+ config._tempBackupDir = tempBackupDir;
560
+ }
561
+
562
+ // For modified files, back them up to temp directory (will be restored as .bak files after install)
563
+ if (modifiedFiles.length > 0) {
564
+ const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
565
+ await fs.ensureDir(tempModifiedBackupDir);
566
+
567
+ spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
568
+ for (const modifiedFile of modifiedFiles) {
569
+ const relativePath = path.relative(bmadDir, modifiedFile.path);
570
+ const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
571
+ await fs.ensureDir(path.dirname(tempBackupPath));
572
+ await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
573
+ }
574
+ spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
575
+
576
+ config._tempModifiedBackupDir = tempModifiedBackupDir;
577
+ }
578
+ }
579
+ } else if (existingInstall.installed && config._quickUpdate) {
580
+ // Quick update mode - automatically treat as update without prompting
581
+ spinner.text = 'Preparing quick update...';
582
+ config._isUpdate = true;
583
+ config._existingInstall = existingInstall;
584
+
585
+ // Detect custom and modified files BEFORE updating
586
+ const existingFilesManifest = await this.readFilesManifest(bmadDir);
587
+ const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
588
+
589
+ config._customFiles = customFiles;
590
+ config._modifiedFiles = modifiedFiles;
591
+
592
+ // Also check cache directory for custom modules (like quick update does)
593
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
594
+ if (await fs.pathExists(cacheDir)) {
595
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
596
+
597
+ for (const cachedModule of cachedModules) {
598
+ if (cachedModule.isDirectory()) {
599
+ const moduleId = cachedModule.name;
600
+
601
+ // Skip if we already have this module from manifest
602
+ if (customModulePaths.has(moduleId)) {
603
+ continue;
604
+ }
605
+
606
+ // Check if this is an external official module - skip cache for those
607
+ const isExternal = await this.moduleManager.isExternalModule(moduleId);
608
+ if (isExternal) {
609
+ // External modules are handled via cloneExternalModule, not from cache
610
+ continue;
611
+ }
612
+
613
+ const cachedPath = path.join(cacheDir, moduleId);
614
+
615
+ // Check if this is actually a custom module (has module.yaml)
616
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
617
+ if (await fs.pathExists(moduleYamlPath)) {
618
+ customModulePaths.set(moduleId, cachedPath);
619
+ }
620
+ }
621
+ }
622
+
623
+ // Update module manager with the new custom module paths from cache
624
+ this.moduleManager.setCustomModulePaths(customModulePaths);
625
+ }
626
+
627
+ // Back up custom files
628
+ if (customFiles.length > 0) {
629
+ const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
630
+ await fs.ensureDir(tempBackupDir);
631
+
632
+ spinner.start(`Backing up ${customFiles.length} custom files...`);
633
+ for (const customFile of customFiles) {
634
+ const relativePath = path.relative(bmadDir, customFile);
635
+ const backupPath = path.join(tempBackupDir, relativePath);
636
+ await fs.ensureDir(path.dirname(backupPath));
637
+ await fs.copy(customFile, backupPath);
638
+ }
639
+ spinner.succeed(`Backed up ${customFiles.length} custom files`);
640
+ config._tempBackupDir = tempBackupDir;
641
+ }
642
+
643
+ // Back up modified files
644
+ if (modifiedFiles.length > 0) {
645
+ const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
646
+ await fs.ensureDir(tempModifiedBackupDir);
647
+
648
+ spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
649
+ for (const modifiedFile of modifiedFiles) {
650
+ const relativePath = path.relative(bmadDir, modifiedFile.path);
651
+ const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
652
+ await fs.ensureDir(path.dirname(tempBackupPath));
653
+ await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
654
+ }
655
+ spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
656
+ config._tempModifiedBackupDir = tempModifiedBackupDir;
657
+ }
658
+ }
659
+
660
+ // Now collect tool configurations after we know if it's a reinstall
661
+ // Skip for quick update since we already have the IDE list
662
+ spinner.stop();
663
+ let toolSelection;
664
+ if (config._quickUpdate) {
665
+ // Quick update already has IDEs configured, use saved configurations
666
+ const preConfiguredIdes = {};
667
+ const savedIdeConfigs = config._savedIdeConfigs || {};
668
+
669
+ for (const ide of config.ides || []) {
670
+ // Use saved config if available, otherwise mark as already configured (legacy)
671
+ if (savedIdeConfigs[ide]) {
672
+ preConfiguredIdes[ide] = savedIdeConfigs[ide];
673
+ } else {
674
+ preConfiguredIdes[ide] = { _alreadyConfigured: true };
675
+ }
676
+ }
677
+ toolSelection = {
678
+ ides: config.ides || [],
679
+ skipIde: !config.ides || config.ides.length === 0,
680
+ configurations: preConfiguredIdes,
681
+ };
682
+ } else {
683
+ // Pass pre-selected IDEs from early prompt (if available)
684
+ // This allows IDE selection to happen before file copying, improving UX
685
+ const preSelectedIdes = config.ides && config.ides.length > 0 ? config.ides : null;
686
+ toolSelection = await this.collectToolConfigurations(
687
+ path.resolve(config.directory),
688
+ config.modules,
689
+ config._isFullReinstall || false,
690
+ config._previouslyConfiguredIdes || [],
691
+ preSelectedIdes,
692
+ );
693
+ }
694
+
695
+ // Merge tool selection into config (for both quick update and regular flow)
696
+ config.ides = toolSelection.ides;
697
+ config.skipIde = toolSelection.skipIde;
698
+ const ideConfigurations = toolSelection.configurations;
699
+
700
+ // Add spacing after prompts before installation progress
701
+ console.log('');
702
+
703
+ if (spinner.isSpinning) {
704
+ spinner.text = 'Continuing installation...';
705
+ } else {
706
+ spinner.start('Continuing installation...');
707
+ }
708
+
709
+ // Create bmad directory structure
710
+ spinner.text = 'Creating directory structure...';
711
+ await this.createDirectoryStructure(bmadDir);
712
+
713
+ // Cache custom modules if any
714
+ if (customModulePaths && customModulePaths.size > 0) {
715
+ spinner.text = 'Caching custom modules...';
716
+ const { CustomModuleCache } = require('./custom-module-cache');
717
+ const customCache = new CustomModuleCache(bmadDir);
718
+
719
+ for (const [moduleId, sourcePath] of customModulePaths) {
720
+ const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
721
+ sourcePath: sourcePath, // Store original path for updates
722
+ });
723
+
724
+ // Update the customModulePaths to use the cached location
725
+ customModulePaths.set(moduleId, cachedInfo.cachePath);
726
+ }
727
+
728
+ // Update module manager with the cached paths
729
+ this.moduleManager.setCustomModulePaths(customModulePaths);
730
+ spinner.succeed('Custom modules cached');
731
+ }
732
+
733
+ const projectRoot = getProjectRoot();
734
+
735
+ // Step 1: Install core module first (if requested)
736
+ if (config.installCore) {
737
+ spinner.start('Installing BMAD core...');
738
+ await this.installCoreWithDependencies(bmadDir, { core: {} });
739
+ spinner.succeed('Core installed');
740
+
741
+ // Generate core config file
742
+ await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
743
+ }
744
+
745
+ // Custom content is already handled in UI before module selection
746
+ let finalCustomContent = config.customContent;
747
+
748
+ // Step 3: Prepare modules list including cached custom modules
749
+ let allModules = [...(config.modules || [])];
750
+
751
+ // During quick update, we might have custom module sources from the manifest
752
+ if (config._customModuleSources) {
753
+ // Add custom modules from stored sources
754
+ for (const [moduleId, customInfo] of config._customModuleSources) {
755
+ if (!allModules.includes(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
756
+ allModules.push(moduleId);
757
+ }
758
+ }
759
+ }
760
+
761
+ // Add cached custom modules
762
+ if (finalCustomContent && finalCustomContent.cachedModules) {
763
+ for (const cachedModule of finalCustomContent.cachedModules) {
764
+ if (!allModules.includes(cachedModule.id)) {
765
+ allModules.push(cachedModule.id);
766
+ }
767
+ }
768
+ }
769
+
770
+ // Regular custom content from user input (non-cached)
771
+ if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
772
+ // Add custom modules to the installation list
773
+ const customHandler = new CustomHandler();
774
+ for (const customFile of finalCustomContent.selectedFiles) {
775
+ const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
776
+ if (customInfo && customInfo.id) {
777
+ allModules.push(customInfo.id);
778
+ }
779
+ }
780
+ }
781
+
782
+ // Don't include core again if already installed
783
+ if (config.installCore) {
784
+ allModules = allModules.filter((m) => m !== 'core');
785
+ }
786
+
787
+ const modulesToInstall = allModules;
788
+
789
+ // For dependency resolution, we only need regular modules (not custom modules)
790
+ // Custom modules are already installed in _bmad and don't need dependency resolution from source
791
+ const regularModulesForResolution = allModules.filter((module) => {
792
+ // Check if this is a custom module
793
+ const isCustom =
794
+ customModulePaths.has(module) ||
795
+ (finalCustomContent && finalCustomContent.cachedModules && finalCustomContent.cachedModules.some((cm) => cm.id === module)) ||
796
+ (finalCustomContent &&
797
+ finalCustomContent.selected &&
798
+ finalCustomContent.selectedFiles &&
799
+ finalCustomContent.selectedFiles.some((f) => f.includes(module)));
800
+ return !isCustom;
801
+ });
802
+
803
+ // For dependency resolution, we need to pass the project root
804
+ // Create a temporary module manager that knows about custom content locations
805
+ const tempModuleManager = new ModuleManager({
806
+ bmadDir: bmadDir, // Pass bmadDir so we can check cache
807
+ });
808
+
809
+ const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
810
+ verbose: config.verbose,
811
+ moduleManager: tempModuleManager,
812
+ });
813
+
814
+ spinner.succeed('Dependencies resolved');
815
+
816
+ // Install modules with their dependencies
817
+ if (allModules && allModules.length > 0) {
818
+ const installedModuleNames = new Set();
819
+
820
+ for (const moduleName of allModules) {
821
+ // Skip if already installed
822
+ if (installedModuleNames.has(moduleName)) {
823
+ continue;
824
+ }
825
+ installedModuleNames.add(moduleName);
826
+
827
+ // Show appropriate message based on whether this is a quick update
828
+ const isQuickUpdate = config._quickUpdate || false;
829
+ spinner.start(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`);
830
+
831
+ // Check if this is a custom module
832
+ let isCustomModule = false;
833
+ let customInfo = null;
834
+ let useCache = false;
835
+
836
+ // First check if we have a cached version
837
+ if (finalCustomContent && finalCustomContent.cachedModules) {
838
+ const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
839
+ if (cachedModule) {
840
+ isCustomModule = true;
841
+ customInfo = {
842
+ id: moduleName,
843
+ path: cachedModule.cachePath,
844
+ config: {},
845
+ };
846
+ useCache = true;
847
+ }
848
+ }
849
+
850
+ // Then check if we have custom module sources from the manifest (for quick update)
851
+ if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
852
+ customInfo = config._customModuleSources.get(moduleName);
853
+ isCustomModule = true;
854
+
855
+ // Check if this is a cached module (source path starts with _config)
856
+ if (
857
+ customInfo.sourcePath &&
858
+ (customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom'))
859
+ ) {
860
+ useCache = true;
861
+ // Make sure we have the right path structure
862
+ if (!customInfo.path) {
863
+ customInfo.path = customInfo.sourcePath;
864
+ }
865
+ }
866
+ }
867
+
868
+ // Finally check regular custom content
869
+ if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
870
+ const customHandler = new CustomHandler();
871
+ for (const customFile of finalCustomContent.selectedFiles) {
872
+ const info = await customHandler.getCustomInfo(customFile, projectDir);
873
+ if (info && info.id === moduleName) {
874
+ isCustomModule = true;
875
+ customInfo = info;
876
+ break;
877
+ }
878
+ }
879
+ }
880
+
881
+ if (isCustomModule && customInfo) {
882
+ // Custom modules are now installed via ModuleManager just like standard modules
883
+ // The custom module path should already be in customModulePaths from earlier setup
884
+ if (!customModulePaths.has(moduleName) && customInfo.path) {
885
+ customModulePaths.set(moduleName, customInfo.path);
886
+ this.moduleManager.setCustomModulePaths(customModulePaths);
887
+ }
888
+
889
+ const collectedModuleConfig = moduleConfigs[moduleName] || {};
890
+
891
+ // Use ModuleManager to install the custom module
892
+ await this.moduleManager.install(
893
+ moduleName,
894
+ bmadDir,
895
+ (filePath) => {
896
+ this.installedFiles.add(filePath);
897
+ },
898
+ {
899
+ isCustom: true,
900
+ moduleConfig: collectedModuleConfig,
901
+ isQuickUpdate: config._quickUpdate || false,
902
+ installer: this,
903
+ },
904
+ );
905
+
906
+ // Create module config (include collected config from module.yaml prompts)
907
+ await this.generateModuleConfigs(bmadDir, {
908
+ [moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
909
+ });
910
+ } else {
911
+ // Regular module installation
912
+ // Special case for core module
913
+ if (moduleName === 'core') {
914
+ await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
915
+ } else {
916
+ await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
917
+ }
918
+ }
919
+
920
+ spinner.succeed(`Module ${isQuickUpdate ? 'updated' : 'installed'}: ${moduleName}`);
921
+ }
922
+
923
+ // Install partial modules (only dependencies)
924
+ for (const [module, files] of Object.entries(resolution.byModule)) {
925
+ if (!allModules.includes(module) && module !== 'core') {
926
+ const totalFiles =
927
+ files.agents.length +
928
+ files.tasks.length +
929
+ files.tools.length +
930
+ files.templates.length +
931
+ files.data.length +
932
+ files.other.length;
933
+ if (totalFiles > 0) {
934
+ spinner.start(`Installing ${module} dependencies...`);
935
+ await this.installPartialModule(module, bmadDir, files);
936
+ spinner.succeed(`${module} dependencies installed`);
937
+ }
938
+ }
939
+ }
940
+ }
941
+
942
+ // All content is now installed as modules - no separate custom content handling needed
943
+
944
+ // Generate clean config.yaml files for each installed module
945
+ spinner.start('Generating module configurations...');
946
+ await this.generateModuleConfigs(bmadDir, moduleConfigs);
947
+ spinner.succeed('Module configurations generated');
948
+
949
+ // Create agent configuration files
950
+ // Note: Legacy createAgentConfigs removed - using YAML customize system instead
951
+ // Customize templates are now created in processAgentFiles when building YAML agents
952
+
953
+ // Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion)
954
+ const cfgDir = path.join(bmadDir, '_config');
955
+ this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
956
+ this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
957
+ this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
958
+ this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
959
+
960
+ // Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup
961
+ // This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
962
+ spinner.start('Generating workflow and agent manifests...');
963
+ const manifestGen = new ManifestGenerator();
964
+
965
+ // For quick update, we need ALL installed modules in the manifest
966
+ // Not just the ones being updated
967
+ const allModulesForManifest = config._quickUpdate
968
+ ? config._existingModules || allModules || []
969
+ : config._preserveModules
970
+ ? [...allModules, ...config._preserveModules]
971
+ : allModules || [];
972
+
973
+ // For regular installs (including when called from quick update), use what we have
974
+ let modulesForCsvPreserve;
975
+ if (config._quickUpdate) {
976
+ // Quick update - use existing modules or fall back to modules being updated
977
+ modulesForCsvPreserve = config._existingModules || allModules || [];
978
+ } else {
979
+ // Regular install - use the modules we're installing plus any preserved ones
980
+ modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
981
+ }
982
+
983
+ const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
984
+ ides: config.ides || [],
985
+ preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
986
+ });
987
+
988
+ // Custom modules are now included in the main modules list - no separate tracking needed
989
+
990
+ spinner.succeed(
991
+ `Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
992
+ );
993
+
994
+ // Merge all module-help.csv files into bmad-help.csv
995
+ // This must happen AFTER generateManifests because it depends on agent-manifest.csv
996
+ spinner.start('Generating workflow help catalog...');
997
+ await this.mergeModuleHelpCatalogs(bmadDir);
998
+ spinner.succeed('Workflow help catalog generated');
999
+
1000
+ // Configure IDEs and copy documentation
1001
+ if (!config.skipIde && config.ides && config.ides.length > 0) {
1002
+ // Ensure IDE manager is initialized (handlers may not be loaded in quick update flow)
1003
+ await this.ideManager.ensureInitialized();
1004
+
1005
+ // Filter out any undefined/null values from the IDE list
1006
+ const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
1007
+
1008
+ if (validIdes.length === 0) {
1009
+ console.log(chalk.yellow('⚠️ No valid IDEs selected. Skipping IDE configuration.'));
1010
+ } else {
1011
+ // Check if any IDE might need prompting (no pre-collected config)
1012
+ const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
1013
+
1014
+ if (!needsPrompting) {
1015
+ spinner.start('Configuring IDEs...');
1016
+ }
1017
+
1018
+ // Temporarily suppress console output if not verbose
1019
+ const originalLog = console.log;
1020
+ if (!config.verbose) {
1021
+ console.log = () => {};
1022
+ }
1023
+
1024
+ for (const ide of validIdes) {
1025
+ // Only show spinner if we have pre-collected config (no prompts expected)
1026
+ if (ideConfigurations[ide] && !needsPrompting) {
1027
+ spinner.text = `Configuring ${ide}...`;
1028
+ } else if (!ideConfigurations[ide]) {
1029
+ // Stop spinner before prompting
1030
+ if (spinner.isSpinning) {
1031
+ spinner.stop();
1032
+ }
1033
+ console.log(chalk.cyan(`\nConfiguring ${ide}...`));
1034
+ }
1035
+
1036
+ // Pass pre-collected configuration to avoid re-prompting
1037
+ await this.ideManager.setup(ide, projectDir, bmadDir, {
1038
+ selectedModules: allModules || [],
1039
+ preCollectedConfig: ideConfigurations[ide] || null,
1040
+ verbose: config.verbose,
1041
+ });
1042
+
1043
+ // Save IDE configuration for future updates
1044
+ if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
1045
+ await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
1046
+ }
1047
+
1048
+ // Restart spinner if we stopped it
1049
+ if (!ideConfigurations[ide] && !spinner.isSpinning) {
1050
+ spinner.start('Configuring IDEs...');
1051
+ }
1052
+ }
1053
+
1054
+ // Restore console.log
1055
+ console.log = originalLog;
1056
+
1057
+ if (spinner.isSpinning) {
1058
+ spinner.succeed(`Configured: ${validIdes.join(', ')}`);
1059
+ } else {
1060
+ console.log(chalk.green(`✓ Configured: ${validIdes.join(', ')}`));
1061
+ }
1062
+ }
1063
+ }
1064
+
1065
+ // Run module-specific installers after IDE setup
1066
+ spinner.start('Running module-specific installers...');
1067
+
1068
+ // Create a conditional logger based on verbose mode
1069
+ const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
1070
+ const moduleLogger = {
1071
+ log: (msg) => (verboseMode ? console.log(msg) : {}), // Only log in verbose mode
1072
+ error: (msg) => console.error(msg), // Always show errors
1073
+ warn: (msg) => console.warn(msg), // Always show warnings
1074
+ };
1075
+
1076
+ // Run core module installer if core was installed
1077
+ if (config.installCore || resolution.byModule.core) {
1078
+ spinner.text = 'Running core module installer...';
1079
+
1080
+ await this.moduleManager.runModuleInstaller('core', bmadDir, {
1081
+ installedIDEs: config.ides || [],
1082
+ moduleConfig: moduleConfigs.core || {},
1083
+ coreConfig: moduleConfigs.core || {},
1084
+ logger: moduleLogger,
1085
+ });
1086
+ }
1087
+
1088
+ // Run installers for user-selected modules
1089
+ if (config.modules && config.modules.length > 0) {
1090
+ for (const moduleName of config.modules) {
1091
+ spinner.text = `Running ${moduleName} module installer...`;
1092
+
1093
+ // Pass installed IDEs and module config to module installer
1094
+ await this.moduleManager.runModuleInstaller(moduleName, bmadDir, {
1095
+ installedIDEs: config.ides || [],
1096
+ moduleConfig: moduleConfigs[moduleName] || {},
1097
+ coreConfig: moduleConfigs.core || {},
1098
+ logger: moduleLogger,
1099
+ });
1100
+ }
1101
+ }
1102
+
1103
+ spinner.succeed('Module-specific installers completed');
1104
+
1105
+ // Note: Manifest files are already created by ManifestGenerator above
1106
+ // No need to create legacy manifest.csv anymore
1107
+
1108
+ // If this was an update, restore custom files
1109
+ let customFiles = [];
1110
+ let modifiedFiles = [];
1111
+ if (config._isUpdate) {
1112
+ if (config._customFiles && config._customFiles.length > 0) {
1113
+ spinner.start(`Restoring ${config._customFiles.length} custom files...`);
1114
+
1115
+ for (const originalPath of config._customFiles) {
1116
+ const relativePath = path.relative(bmadDir, originalPath);
1117
+ const backupPath = path.join(config._tempBackupDir, relativePath);
1118
+
1119
+ if (await fs.pathExists(backupPath)) {
1120
+ await fs.ensureDir(path.dirname(originalPath));
1121
+ await fs.copy(backupPath, originalPath, { overwrite: true });
1122
+ }
1123
+ }
1124
+
1125
+ // Clean up temp backup
1126
+ if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
1127
+ await fs.remove(config._tempBackupDir);
1128
+ }
1129
+
1130
+ spinner.succeed(`Restored ${config._customFiles.length} custom files`);
1131
+ customFiles = config._customFiles;
1132
+ }
1133
+
1134
+ if (config._modifiedFiles && config._modifiedFiles.length > 0) {
1135
+ modifiedFiles = config._modifiedFiles;
1136
+
1137
+ // Restore modified files as .bak files
1138
+ if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
1139
+ spinner.start(`Restoring ${modifiedFiles.length} modified files as .bak...`);
1140
+
1141
+ for (const modifiedFile of modifiedFiles) {
1142
+ const relativePath = path.relative(bmadDir, modifiedFile.path);
1143
+ const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath);
1144
+ const bakPath = modifiedFile.path + '.bak';
1145
+
1146
+ if (await fs.pathExists(tempBackupPath)) {
1147
+ await fs.ensureDir(path.dirname(bakPath));
1148
+ await fs.copy(tempBackupPath, bakPath, { overwrite: true });
1149
+ }
1150
+ }
1151
+
1152
+ // Clean up temp backup
1153
+ await fs.remove(config._tempModifiedBackupDir);
1154
+
1155
+ spinner.succeed(`Restored ${modifiedFiles.length} modified files as .bak`);
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ spinner.stop();
1161
+
1162
+ // Report custom and modified files if any were found
1163
+ if (customFiles.length > 0) {
1164
+ console.log(chalk.cyan(`\n📁 Custom files preserved: ${customFiles.length}`));
1165
+ }
1166
+
1167
+ if (modifiedFiles.length > 0) {
1168
+ console.log(chalk.yellow(`\n⚠️ User modified files detected: ${modifiedFiles.length}`));
1169
+ console.log(
1170
+ chalk.dim(
1171
+ '\nThese user modified files have been updated with the new version, search the project for .bak files that had your customizations.',
1172
+ ),
1173
+ );
1174
+ console.log(chalk.dim('Remove these .bak files it no longer needed\n'));
1175
+ }
1176
+
1177
+ // Display completion message
1178
+ const { UI } = require('../../../lib/ui');
1179
+ const ui = new UI();
1180
+ ui.showInstallSummary({
1181
+ path: bmadDir,
1182
+ modules: config.modules,
1183
+ ides: config.ides,
1184
+ customFiles: customFiles.length > 0 ? customFiles : undefined,
1185
+ });
1186
+
1187
+ return {
1188
+ success: true,
1189
+ path: bmadDir,
1190
+ modules: config.modules,
1191
+ ides: config.ides,
1192
+ projectDir: projectDir,
1193
+ };
1194
+ } catch (error) {
1195
+ spinner.fail('Installation failed');
1196
+ throw error;
1197
+ }
1198
+ }
1199
+
1200
+ /**
1201
+ * Update existing installation
1202
+ */
1203
+ async update(config) {
1204
+ const spinner = ora('Checking installation...').start();
1205
+
1206
+ try {
1207
+ const projectDir = path.resolve(config.directory);
1208
+ const { bmadDir } = await this.findBmadDir(projectDir);
1209
+ const existingInstall = await this.detector.detect(bmadDir);
1210
+
1211
+ if (!existingInstall.installed) {
1212
+ spinner.fail('No BMAD installation found');
1213
+ throw new Error(`No BMAD installation found at ${bmadDir}`);
1214
+ }
1215
+
1216
+ spinner.text = 'Analyzing update requirements...';
1217
+
1218
+ // Compare versions and determine what needs updating
1219
+ const currentVersion = existingInstall.version;
1220
+ const newVersion = require(path.join(getProjectRoot(), 'package.json')).version;
1221
+
1222
+ // Check for custom modules with missing sources before update
1223
+ const customModuleSources = new Map();
1224
+
1225
+ // Check manifest for backward compatibility
1226
+ if (existingInstall.customModules) {
1227
+ for (const customModule of existingInstall.customModules) {
1228
+ customModuleSources.set(customModule.id, customModule);
1229
+ }
1230
+ }
1231
+
1232
+ // Also check cache directory
1233
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
1234
+ if (await fs.pathExists(cacheDir)) {
1235
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
1236
+
1237
+ for (const cachedModule of cachedModules) {
1238
+ if (cachedModule.isDirectory()) {
1239
+ const moduleId = cachedModule.name;
1240
+
1241
+ // Skip if we already have this module
1242
+ if (customModuleSources.has(moduleId)) {
1243
+ continue;
1244
+ }
1245
+
1246
+ // Check if this is an external official module - skip cache for those
1247
+ const isExternal = await this.moduleManager.isExternalModule(moduleId);
1248
+ if (isExternal) {
1249
+ // External modules are handled via cloneExternalModule, not from cache
1250
+ continue;
1251
+ }
1252
+
1253
+ const cachedPath = path.join(cacheDir, moduleId);
1254
+
1255
+ // Check if this is actually a custom module (has module.yaml)
1256
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
1257
+ if (await fs.pathExists(moduleYamlPath)) {
1258
+ customModuleSources.set(moduleId, {
1259
+ id: moduleId,
1260
+ name: moduleId,
1261
+ sourcePath: path.join('_config', 'custom', moduleId), // Relative path
1262
+ cached: true,
1263
+ });
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+
1269
+ if (customModuleSources.size > 0) {
1270
+ spinner.stop();
1271
+ console.log(chalk.yellow('\nChecking custom module sources before update...'));
1272
+
1273
+ const projectRoot = getProjectRoot();
1274
+ await this.handleMissingCustomSources(
1275
+ customModuleSources,
1276
+ bmadDir,
1277
+ projectRoot,
1278
+ 'update',
1279
+ existingInstall.modules.map((m) => m.id),
1280
+ );
1281
+
1282
+ spinner.start('Preparing update...');
1283
+ }
1284
+
1285
+ if (config.dryRun) {
1286
+ spinner.stop();
1287
+ console.log(chalk.cyan('\n🔍 Update Preview (Dry Run)\n'));
1288
+ console.log(chalk.bold('Current version:'), currentVersion);
1289
+ console.log(chalk.bold('New version:'), newVersion);
1290
+ console.log(chalk.bold('Core:'), existingInstall.hasCore ? 'Will be updated' : 'Not installed');
1291
+
1292
+ if (existingInstall.modules.length > 0) {
1293
+ console.log(chalk.bold('\nModules to update:'));
1294
+ for (const mod of existingInstall.modules) {
1295
+ console.log(` - ${mod.id}`);
1296
+ }
1297
+ }
1298
+ return;
1299
+ }
1300
+
1301
+ // Perform actual update
1302
+ if (existingInstall.hasCore) {
1303
+ spinner.text = 'Updating core...';
1304
+ await this.updateCore(bmadDir, config.force);
1305
+ }
1306
+
1307
+ for (const module of existingInstall.modules) {
1308
+ spinner.text = `Updating module: ${module.id}...`;
1309
+ await this.moduleManager.update(module.id, bmadDir, config.force);
1310
+ }
1311
+
1312
+ // Update manifest
1313
+ spinner.text = 'Updating manifest...';
1314
+ await this.manifest.update(bmadDir, {
1315
+ version: newVersion,
1316
+ updateDate: new Date().toISOString(),
1317
+ });
1318
+
1319
+ spinner.succeed('Update complete');
1320
+ return { success: true };
1321
+ } catch (error) {
1322
+ spinner.fail('Update failed');
1323
+ throw error;
1324
+ }
1325
+ }
1326
+
1327
+ /**
1328
+ * Get installation status
1329
+ */
1330
+ async getStatus(directory) {
1331
+ const projectDir = path.resolve(directory);
1332
+ const { bmadDir } = await this.findBmadDir(projectDir);
1333
+ return await this.detector.detect(bmadDir);
1334
+ }
1335
+
1336
+ /**
1337
+ * Get available modules
1338
+ */
1339
+ async getAvailableModules() {
1340
+ return await this.moduleManager.listAvailable();
1341
+ }
1342
+
1343
+ /**
1344
+ * Uninstall BMAD
1345
+ */
1346
+ async uninstall(directory) {
1347
+ const projectDir = path.resolve(directory);
1348
+ const { bmadDir } = await this.findBmadDir(projectDir);
1349
+
1350
+ if (await fs.pathExists(bmadDir)) {
1351
+ await fs.remove(bmadDir);
1352
+ }
1353
+
1354
+ // Clean up IDE configurations
1355
+ await this.ideManager.cleanup(projectDir);
1356
+
1357
+ return { success: true };
1358
+ }
1359
+
1360
+ /**
1361
+ * Private: Create directory structure
1362
+ */
1363
+ /**
1364
+ * Merge all module-help.csv files into a single bmad-help.csv
1365
+ * Scans all installed modules for module-help.csv and merges them
1366
+ * Enriches agent info from agent-manifest.csv
1367
+ * Output is written to _bmad/_config/bmad-help.csv
1368
+ * @param {string} bmadDir - BMAD installation directory
1369
+ */
1370
+ async mergeModuleHelpCatalogs(bmadDir) {
1371
+ const allRows = [];
1372
+ const headerRow =
1373
+ 'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs';
1374
+
1375
+ // Load agent manifest for agent info lookup
1376
+ const agentManifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
1377
+ const agentInfo = new Map(); // agent-name -> {command, displayName, title+icon}
1378
+
1379
+ if (await fs.pathExists(agentManifestPath)) {
1380
+ const manifestContent = await fs.readFile(agentManifestPath, 'utf8');
1381
+ const lines = manifestContent.split('\n').filter((line) => line.trim());
1382
+
1383
+ for (const line of lines) {
1384
+ if (line.startsWith('name,')) continue; // Skip header
1385
+
1386
+ const cols = line.split(',');
1387
+ if (cols.length >= 4) {
1388
+ const agentName = cols[0].replaceAll('"', '').trim();
1389
+ const displayName = cols[1].replaceAll('"', '').trim();
1390
+ const title = cols[2].replaceAll('"', '').trim();
1391
+ const icon = cols[3].replaceAll('"', '').trim();
1392
+ const module = cols[10] ? cols[10].replaceAll('"', '').trim() : '';
1393
+
1394
+ // Build agent command: bmad:module:agent:name
1395
+ const agentCommand = module ? `bmad:${module}:agent:${agentName}` : `bmad:agent:${agentName}`;
1396
+
1397
+ agentInfo.set(agentName, {
1398
+ command: agentCommand,
1399
+ displayName: displayName || agentName,
1400
+ title: icon && title ? `${icon} ${title}` : title || agentName,
1401
+ });
1402
+ }
1403
+ }
1404
+ }
1405
+
1406
+ // Get all installed module directories
1407
+ const entries = await fs.readdir(bmadDir, { withFileTypes: true });
1408
+ const installedModules = entries
1409
+ .filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs' && entry.name !== '_memory')
1410
+ .map((entry) => entry.name);
1411
+
1412
+ // Add core module to scan (it's installed at root level as _config, but we check src/core)
1413
+ const coreModulePath = getSourcePath('core');
1414
+ const modulePaths = new Map();
1415
+
1416
+ // Map all module source paths
1417
+ if (await fs.pathExists(coreModulePath)) {
1418
+ modulePaths.set('core', coreModulePath);
1419
+ }
1420
+
1421
+ // Map installed module paths
1422
+ for (const moduleName of installedModules) {
1423
+ const modulePath = path.join(bmadDir, moduleName);
1424
+ modulePaths.set(moduleName, modulePath);
1425
+ }
1426
+
1427
+ // Scan each module for module-help.csv
1428
+ for (const [moduleName, modulePath] of modulePaths) {
1429
+ const helpFilePath = path.join(modulePath, 'module-help.csv');
1430
+
1431
+ if (await fs.pathExists(helpFilePath)) {
1432
+ try {
1433
+ const content = await fs.readFile(helpFilePath, 'utf8');
1434
+ const lines = content.split('\n').filter((line) => line.trim() && !line.startsWith('#'));
1435
+
1436
+ for (const line of lines) {
1437
+ // Skip header row
1438
+ if (line.startsWith('module,')) {
1439
+ continue;
1440
+ }
1441
+
1442
+ // Parse the line - handle quoted fields with commas
1443
+ const columns = this.parseCSVLine(line);
1444
+ if (columns.length >= 12) {
1445
+ // Map old schema to new schema
1446
+ // Old: module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
1447
+ // New: module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs
1448
+
1449
+ const [
1450
+ module,
1451
+ phase,
1452
+ name,
1453
+ code,
1454
+ sequence,
1455
+ workflowFile,
1456
+ command,
1457
+ required,
1458
+ agentName,
1459
+ options,
1460
+ description,
1461
+ outputLocation,
1462
+ outputs,
1463
+ ] = columns;
1464
+
1465
+ // If module column is empty, set it to this module's name (except for core which stays empty for universal tools)
1466
+ const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
1467
+
1468
+ // Lookup agent info
1469
+ const cleanAgentName = agentName ? agentName.trim() : '';
1470
+ const agentData = agentInfo.get(cleanAgentName) || { command: '', displayName: '', title: '' };
1471
+
1472
+ // Build new row with agent info
1473
+ const newRow = [
1474
+ finalModule,
1475
+ phase || '',
1476
+ name || '',
1477
+ code || '',
1478
+ sequence || '',
1479
+ workflowFile || '',
1480
+ command || '',
1481
+ required || 'false',
1482
+ cleanAgentName,
1483
+ agentData.command,
1484
+ agentData.displayName,
1485
+ agentData.title,
1486
+ options || '',
1487
+ description || '',
1488
+ outputLocation || '',
1489
+ outputs || '',
1490
+ ];
1491
+
1492
+ allRows.push(newRow.map((c) => this.escapeCSVField(c)).join(','));
1493
+ }
1494
+ }
1495
+
1496
+ if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
1497
+ console.log(chalk.dim(` Merged module-help from: ${moduleName}`));
1498
+ }
1499
+ } catch (error) {
1500
+ console.warn(chalk.yellow(` Warning: Failed to read module-help.csv from ${moduleName}:`, error.message));
1501
+ }
1502
+ }
1503
+ }
1504
+
1505
+ // Sort by module, then phase, then sequence
1506
+ allRows.sort((a, b) => {
1507
+ const colsA = this.parseCSVLine(a);
1508
+ const colsB = this.parseCSVLine(b);
1509
+
1510
+ // Module comparison (empty module/universal tools come first)
1511
+ const moduleA = (colsA[0] || '').toLowerCase();
1512
+ const moduleB = (colsB[0] || '').toLowerCase();
1513
+ if (moduleA !== moduleB) {
1514
+ return moduleA.localeCompare(moduleB);
1515
+ }
1516
+
1517
+ // Phase comparison
1518
+ const phaseA = colsA[1] || '';
1519
+ const phaseB = colsB[1] || '';
1520
+ if (phaseA !== phaseB) {
1521
+ return phaseA.localeCompare(phaseB);
1522
+ }
1523
+
1524
+ // Sequence comparison
1525
+ const seqA = parseInt(colsA[4] || '0', 10);
1526
+ const seqB = parseInt(colsB[4] || '0', 10);
1527
+ return seqA - seqB;
1528
+ });
1529
+
1530
+ // Write merged catalog
1531
+ const outputDir = path.join(bmadDir, '_config');
1532
+ await fs.ensureDir(outputDir);
1533
+ const outputPath = path.join(outputDir, 'bmad-help.csv');
1534
+
1535
+ const mergedContent = [headerRow, ...allRows].join('\n');
1536
+ await fs.writeFile(outputPath, mergedContent, 'utf8');
1537
+
1538
+ // Track the installed file
1539
+ this.installedFiles.add(outputPath);
1540
+
1541
+ if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
1542
+ console.log(chalk.dim(` Generated bmad-help.csv: ${allRows.length} workflows`));
1543
+ }
1544
+ }
1545
+
1546
+ /**
1547
+ * Parse a CSV line, handling quoted fields
1548
+ * @param {string} line - CSV line to parse
1549
+ * @returns {Array} Array of field values
1550
+ */
1551
+ parseCSVLine(line) {
1552
+ const result = [];
1553
+ let current = '';
1554
+ let inQuotes = false;
1555
+
1556
+ for (let i = 0; i < line.length; i++) {
1557
+ const char = line[i];
1558
+ const nextChar = line[i + 1];
1559
+
1560
+ if (char === '"') {
1561
+ if (inQuotes && nextChar === '"') {
1562
+ // Escaped quote
1563
+ current += '"';
1564
+ i++; // Skip next quote
1565
+ } else {
1566
+ // Toggle quote mode
1567
+ inQuotes = !inQuotes;
1568
+ }
1569
+ } else if (char === ',' && !inQuotes) {
1570
+ result.push(current);
1571
+ current = '';
1572
+ } else {
1573
+ current += char;
1574
+ }
1575
+ }
1576
+ result.push(current);
1577
+ return result;
1578
+ }
1579
+
1580
+ /**
1581
+ * Escape a CSV field if it contains special characters
1582
+ * @param {string} field - Field value to escape
1583
+ * @returns {string} Escaped field
1584
+ */
1585
+ escapeCSVField(field) {
1586
+ if (field === null || field === undefined) {
1587
+ return '';
1588
+ }
1589
+ const str = String(field);
1590
+ // If field contains comma, quote, or newline, wrap in quotes and escape inner quotes
1591
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
1592
+ return `"${str.replaceAll('"', '""')}"`;
1593
+ }
1594
+ return str;
1595
+ }
1596
+
1597
+ async createDirectoryStructure(bmadDir) {
1598
+ await fs.ensureDir(bmadDir);
1599
+ await fs.ensureDir(path.join(bmadDir, '_config'));
1600
+ await fs.ensureDir(path.join(bmadDir, '_config', 'agents'));
1601
+ await fs.ensureDir(path.join(bmadDir, '_config', 'custom'));
1602
+ }
1603
+
1604
+ /**
1605
+ * Generate clean config.yaml files for each installed module
1606
+ * @param {string} bmadDir - BMAD installation directory
1607
+ * @param {Object} moduleConfigs - Collected configuration values
1608
+ */
1609
+ async generateModuleConfigs(bmadDir, moduleConfigs) {
1610
+ const yaml = require('yaml');
1611
+
1612
+ // Extract core config values to share with other modules
1613
+ const coreConfig = moduleConfigs.core || {};
1614
+
1615
+ // Get all installed module directories
1616
+ const entries = await fs.readdir(bmadDir, { withFileTypes: true });
1617
+ const installedModules = entries
1618
+ .filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs')
1619
+ .map((entry) => entry.name);
1620
+
1621
+ // Generate config.yaml for each installed module
1622
+ for (const moduleName of installedModules) {
1623
+ const modulePath = path.join(bmadDir, moduleName);
1624
+
1625
+ // Get module-specific config or use empty object if none
1626
+ const config = moduleConfigs[moduleName] || {};
1627
+
1628
+ if (await fs.pathExists(modulePath)) {
1629
+ const configPath = path.join(modulePath, 'config.yaml');
1630
+
1631
+ // Create header
1632
+ const packageJson = require(path.join(getProjectRoot(), 'package.json'));
1633
+ const header = `# ${moduleName.toUpperCase()} Module Configuration
1634
+ # Generated by BMAD installer
1635
+ # Version: ${packageJson.version}
1636
+ # Date: ${new Date().toISOString()}
1637
+
1638
+ `;
1639
+
1640
+ // For non-core modules, add core config values directly
1641
+ let finalConfig = { ...config };
1642
+ let coreSection = '';
1643
+
1644
+ if (moduleName !== 'core' && coreConfig && Object.keys(coreConfig).length > 0) {
1645
+ // Add core values directly to the module config
1646
+ // These will be available for reference in the module
1647
+ finalConfig = {
1648
+ ...config,
1649
+ ...coreConfig, // Spread core config values directly into the module config
1650
+ };
1651
+
1652
+ // Create a comment section to identify core values
1653
+ coreSection = '\n# Core Configuration Values\n';
1654
+ }
1655
+
1656
+ // Clean the config to remove any non-serializable values (like functions)
1657
+ const cleanConfig = structuredClone(finalConfig);
1658
+
1659
+ // Convert config to YAML
1660
+ let yamlContent = yaml.stringify(cleanConfig, {
1661
+ indent: 2,
1662
+ lineWidth: 0,
1663
+ minContentWidth: 0,
1664
+ });
1665
+
1666
+ // If we have core values, reorganize the YAML to group them with their comment
1667
+ if (coreSection && moduleName !== 'core') {
1668
+ // Split the YAML into lines
1669
+ const lines = yamlContent.split('\n');
1670
+ const moduleConfigLines = [];
1671
+ const coreConfigLines = [];
1672
+
1673
+ // Separate module-specific and core config lines
1674
+ for (const line of lines) {
1675
+ const key = line.split(':')[0].trim();
1676
+ if (Object.prototype.hasOwnProperty.call(coreConfig, key)) {
1677
+ coreConfigLines.push(line);
1678
+ } else {
1679
+ moduleConfigLines.push(line);
1680
+ }
1681
+ }
1682
+
1683
+ // Rebuild YAML with module config first, then core config with comment
1684
+ yamlContent = moduleConfigLines.join('\n');
1685
+ if (coreConfigLines.length > 0) {
1686
+ yamlContent += coreSection + coreConfigLines.join('\n');
1687
+ }
1688
+ }
1689
+
1690
+ // Write the clean config file with POSIX-compliant final newline
1691
+ const content = header + yamlContent;
1692
+ await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8');
1693
+
1694
+ // Track the config file in installedFiles
1695
+ this.installedFiles.add(configPath);
1696
+ }
1697
+ }
1698
+ }
1699
+
1700
+ /**
1701
+ * Install core with resolved dependencies
1702
+ * @param {string} bmadDir - BMAD installation directory
1703
+ * @param {Object} coreFiles - Core files to install
1704
+ */
1705
+ async installCoreWithDependencies(bmadDir, coreFiles) {
1706
+ const sourcePath = getModulePath('core');
1707
+ const targetPath = path.join(bmadDir, 'core');
1708
+ await this.installCore(bmadDir);
1709
+ }
1710
+
1711
+ /**
1712
+ * Install module with resolved dependencies
1713
+ * @param {string} moduleName - Module name
1714
+ * @param {string} bmadDir - BMAD installation directory
1715
+ * @param {Object} moduleFiles - Module files to install
1716
+ */
1717
+ async installModuleWithDependencies(moduleName, bmadDir, moduleFiles) {
1718
+ // Get module configuration for conditional installation
1719
+ const moduleConfig = this.configCollector.collectedConfig[moduleName] || {};
1720
+
1721
+ // Use existing module manager for full installation with file tracking
1722
+ // Note: Module-specific installers are called separately after IDE setup
1723
+ await this.moduleManager.install(
1724
+ moduleName,
1725
+ bmadDir,
1726
+ (filePath) => {
1727
+ this.installedFiles.add(filePath);
1728
+ },
1729
+ {
1730
+ skipModuleInstaller: true, // We'll run it later after IDE setup
1731
+ moduleConfig: moduleConfig, // Pass module config for conditional filtering
1732
+ installer: this,
1733
+ },
1734
+ );
1735
+
1736
+ // Process agent files to build YAML agents and create customize templates
1737
+ const modulePath = path.join(bmadDir, moduleName);
1738
+ await this.processAgentFiles(modulePath, moduleName);
1739
+
1740
+ // Dependencies are already included in full module install
1741
+ }
1742
+
1743
+ /**
1744
+ * Install partial module (only dependencies needed by other modules)
1745
+ */
1746
+ async installPartialModule(moduleName, bmadDir, files) {
1747
+ const sourceBase = getModulePath(moduleName);
1748
+ const targetBase = path.join(bmadDir, moduleName);
1749
+
1750
+ // Create module directory
1751
+ await fs.ensureDir(targetBase);
1752
+
1753
+ // Copy only the required dependency files
1754
+ if (files.agents && files.agents.length > 0) {
1755
+ const agentsDir = path.join(targetBase, 'agents');
1756
+ await fs.ensureDir(agentsDir);
1757
+
1758
+ for (const agentPath of files.agents) {
1759
+ const fileName = path.basename(agentPath);
1760
+ const sourcePath = path.join(sourceBase, 'agents', fileName);
1761
+ const targetPath = path.join(agentsDir, fileName);
1762
+
1763
+ if (await fs.pathExists(sourcePath)) {
1764
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
1765
+ this.installedFiles.add(targetPath);
1766
+ }
1767
+ }
1768
+ }
1769
+
1770
+ if (files.tasks && files.tasks.length > 0) {
1771
+ const tasksDir = path.join(targetBase, 'tasks');
1772
+ await fs.ensureDir(tasksDir);
1773
+
1774
+ for (const taskPath of files.tasks) {
1775
+ const fileName = path.basename(taskPath);
1776
+ const sourcePath = path.join(sourceBase, 'tasks', fileName);
1777
+ const targetPath = path.join(tasksDir, fileName);
1778
+
1779
+ if (await fs.pathExists(sourcePath)) {
1780
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
1781
+ this.installedFiles.add(targetPath);
1782
+ }
1783
+ }
1784
+ }
1785
+
1786
+ if (files.tools && files.tools.length > 0) {
1787
+ const toolsDir = path.join(targetBase, 'tools');
1788
+ await fs.ensureDir(toolsDir);
1789
+
1790
+ for (const toolPath of files.tools) {
1791
+ const fileName = path.basename(toolPath);
1792
+ const sourcePath = path.join(sourceBase, 'tools', fileName);
1793
+ const targetPath = path.join(toolsDir, fileName);
1794
+
1795
+ if (await fs.pathExists(sourcePath)) {
1796
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
1797
+ this.installedFiles.add(targetPath);
1798
+ }
1799
+ }
1800
+ }
1801
+
1802
+ if (files.templates && files.templates.length > 0) {
1803
+ const templatesDir = path.join(targetBase, 'templates');
1804
+ await fs.ensureDir(templatesDir);
1805
+
1806
+ for (const templatePath of files.templates) {
1807
+ const fileName = path.basename(templatePath);
1808
+ const sourcePath = path.join(sourceBase, 'templates', fileName);
1809
+ const targetPath = path.join(templatesDir, fileName);
1810
+
1811
+ if (await fs.pathExists(sourcePath)) {
1812
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
1813
+ this.installedFiles.add(targetPath);
1814
+ }
1815
+ }
1816
+ }
1817
+
1818
+ if (files.data && files.data.length > 0) {
1819
+ for (const dataPath of files.data) {
1820
+ // Preserve directory structure for data files
1821
+ const relative = path.relative(sourceBase, dataPath);
1822
+ const targetPath = path.join(targetBase, relative);
1823
+
1824
+ await fs.ensureDir(path.dirname(targetPath));
1825
+
1826
+ if (await fs.pathExists(dataPath)) {
1827
+ await this.copyFileWithPlaceholderReplacement(dataPath, targetPath);
1828
+ this.installedFiles.add(targetPath);
1829
+ }
1830
+ }
1831
+ }
1832
+
1833
+ // Create a marker file to indicate this is a partial installation
1834
+ const markerPath = path.join(targetBase, '.partial');
1835
+ await fs.writeFile(
1836
+ markerPath,
1837
+ `This module contains only dependencies required by other modules.\nInstalled: ${new Date().toISOString()}\n`,
1838
+ );
1839
+ }
1840
+
1841
+ /**
1842
+ * Private: Install core
1843
+ * @param {string} bmadDir - BMAD installation directory
1844
+ */
1845
+ async installCore(bmadDir) {
1846
+ const sourcePath = getModulePath('core');
1847
+ const targetPath = path.join(bmadDir, 'core');
1848
+
1849
+ // Copy core files (skip .agent.yaml files like modules do)
1850
+ await this.copyCoreFiles(sourcePath, targetPath);
1851
+
1852
+ // Compile agents using the same compiler as modules
1853
+ const { ModuleManager } = require('../modules/manager');
1854
+ const moduleManager = new ModuleManager();
1855
+ await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir, this);
1856
+
1857
+ // Process agent files to inject activation block
1858
+ await this.processAgentFiles(targetPath, 'core');
1859
+ }
1860
+
1861
+ /**
1862
+ * Copy core files (similar to copyModuleWithFiltering but for core)
1863
+ * @param {string} sourcePath - Source path
1864
+ * @param {string} targetPath - Target path
1865
+ */
1866
+ async copyCoreFiles(sourcePath, targetPath) {
1867
+ // Get all files in source
1868
+ const files = await this.getFileList(sourcePath);
1869
+
1870
+ for (const file of files) {
1871
+ // Skip sub-modules directory - these are IDE-specific and handled separately
1872
+ if (file.startsWith('sub-modules/')) {
1873
+ continue;
1874
+ }
1875
+
1876
+ // Skip sidecar directories - they are handled separately during agent compilation
1877
+ if (
1878
+ path
1879
+ .dirname(file)
1880
+ .split('/')
1881
+ .some((dir) => dir.toLowerCase().includes('sidecar'))
1882
+ ) {
1883
+ continue;
1884
+ }
1885
+
1886
+ // Skip _module-installer directory - it's only needed at install time
1887
+ if (file.startsWith('_module-installer/') || file === 'module.yaml') {
1888
+ continue;
1889
+ }
1890
+
1891
+ // Skip config.yaml templates - we'll generate clean ones with actual values
1892
+ if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) {
1893
+ continue;
1894
+ }
1895
+
1896
+ // Skip .agent.yaml files - they will be compiled separately
1897
+ if (file.endsWith('.agent.yaml')) {
1898
+ continue;
1899
+ }
1900
+
1901
+ const sourceFile = path.join(sourcePath, file);
1902
+ const targetFile = path.join(targetPath, file);
1903
+
1904
+ // Check if this is an agent file
1905
+ if (file.startsWith('agents/') && file.endsWith('.md')) {
1906
+ // Read the file to check for localskip
1907
+ const content = await fs.readFile(sourceFile, 'utf8');
1908
+
1909
+ // Check for localskip="true" in the agent tag
1910
+ const agentMatch = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
1911
+ if (agentMatch) {
1912
+ console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`));
1913
+ continue; // Skip this agent
1914
+ }
1915
+ }
1916
+
1917
+ // Copy the file with placeholder replacement
1918
+ await fs.ensureDir(path.dirname(targetFile));
1919
+ await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
1920
+
1921
+ // Track the installed file
1922
+ this.installedFiles.add(targetFile);
1923
+ }
1924
+ }
1925
+
1926
+ /**
1927
+ * Get list of all files in a directory recursively
1928
+ * @param {string} dir - Directory path
1929
+ * @param {string} baseDir - Base directory for relative paths
1930
+ * @returns {Array} List of relative file paths
1931
+ */
1932
+ async getFileList(dir, baseDir = dir) {
1933
+ const files = [];
1934
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1935
+
1936
+ for (const entry of entries) {
1937
+ const fullPath = path.join(dir, entry.name);
1938
+
1939
+ if (entry.isDirectory()) {
1940
+ // Skip _module-installer directories
1941
+ if (entry.name === '_module-installer') {
1942
+ continue;
1943
+ }
1944
+ const subFiles = await this.getFileList(fullPath, baseDir);
1945
+ files.push(...subFiles);
1946
+ } else {
1947
+ files.push(path.relative(baseDir, fullPath));
1948
+ }
1949
+ }
1950
+
1951
+ return files;
1952
+ }
1953
+
1954
+ /**
1955
+ * Process agent files to build YAML agents and inject activation blocks
1956
+ * @param {string} modulePath - Path to module in bmad/ installation
1957
+ * @param {string} moduleName - Module name
1958
+ */
1959
+ async processAgentFiles(modulePath, moduleName) {
1960
+ const agentsPath = path.join(modulePath, 'agents');
1961
+
1962
+ // Check if agents directory exists
1963
+ if (!(await fs.pathExists(agentsPath))) {
1964
+ return; // No agents to process
1965
+ }
1966
+
1967
+ // Determine project directory (parent of bmad/ directory)
1968
+ const bmadDir = path.dirname(modulePath);
1969
+ const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
1970
+
1971
+ // Ensure _config/agents directory exists
1972
+ await fs.ensureDir(cfgAgentsDir);
1973
+
1974
+ // Get all agent files
1975
+ const agentFiles = await fs.readdir(agentsPath);
1976
+
1977
+ for (const agentFile of agentFiles) {
1978
+ // Skip .agent.yaml files - they should already be compiled by compileModuleAgents
1979
+ if (agentFile.endsWith('.agent.yaml')) {
1980
+ continue;
1981
+ }
1982
+
1983
+ // Only process .md files (already compiled from YAML)
1984
+ if (!agentFile.endsWith('.md')) {
1985
+ continue;
1986
+ }
1987
+
1988
+ const agentName = agentFile.replace('.md', '');
1989
+ const mdPath = path.join(agentsPath, agentFile);
1990
+ const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
1991
+
1992
+ // For .md files that are already compiled, we don't need to do much
1993
+ // Just ensure the customize template exists
1994
+ if (!(await fs.pathExists(customizePath))) {
1995
+ const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
1996
+ if (await fs.pathExists(genericTemplatePath)) {
1997
+ await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
1998
+ if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
1999
+ console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
2000
+ }
2001
+ }
2002
+ }
2003
+ }
2004
+ }
2005
+
2006
+ /**
2007
+ * Private: Update core
2008
+ */
2009
+ async updateCore(bmadDir, force = false) {
2010
+ const sourcePath = getModulePath('core');
2011
+ const targetPath = path.join(bmadDir, 'core');
2012
+
2013
+ if (force) {
2014
+ await fs.remove(targetPath);
2015
+ await this.installCore(bmadDir);
2016
+ } else {
2017
+ // Selective update - preserve user modifications
2018
+ await this.fileOps.syncDirectory(sourcePath, targetPath);
2019
+
2020
+ // Recompile agents (#1133)
2021
+ const { ModuleManager } = require('../modules/manager');
2022
+ const moduleManager = new ModuleManager();
2023
+ await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir, this);
2024
+ await this.processAgentFiles(targetPath, 'core');
2025
+ }
2026
+ }
2027
+
2028
+ /**
2029
+ * Quick update method - preserves all settings and only prompts for new config fields
2030
+ * @param {Object} config - Configuration with directory
2031
+ * @returns {Object} Update result
2032
+ */
2033
+ async quickUpdate(config) {
2034
+ const ora = require('ora');
2035
+ const spinner = ora('Starting quick update...').start();
2036
+
2037
+ try {
2038
+ const projectDir = path.resolve(config.directory);
2039
+ const { bmadDir } = await this.findBmadDir(projectDir);
2040
+
2041
+ // Check if bmad directory exists
2042
+ if (!(await fs.pathExists(bmadDir))) {
2043
+ spinner.fail('No BMAD installation found');
2044
+ throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
2045
+ }
2046
+
2047
+ spinner.text = 'Detecting installed modules and configuration...';
2048
+
2049
+ // Detect existing installation
2050
+ const existingInstall = await this.detector.detect(bmadDir);
2051
+ const installedModules = existingInstall.modules.map((m) => m.id);
2052
+ const configuredIdes = existingInstall.ides || [];
2053
+ const projectRoot = path.dirname(bmadDir);
2054
+
2055
+ // Get custom module sources from cache
2056
+ const customModuleSources = new Map();
2057
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
2058
+ if (await fs.pathExists(cacheDir)) {
2059
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
2060
+
2061
+ for (const cachedModule of cachedModules) {
2062
+ if (cachedModule.isDirectory()) {
2063
+ const moduleId = cachedModule.name;
2064
+
2065
+ // Skip if we already have this module from manifest
2066
+ if (customModuleSources.has(moduleId)) {
2067
+ continue;
2068
+ }
2069
+
2070
+ // Check if this is an external official module - skip cache for those
2071
+ const isExternal = await this.moduleManager.isExternalModule(moduleId);
2072
+ if (isExternal) {
2073
+ // External modules are handled via cloneExternalModule, not from cache
2074
+ continue;
2075
+ }
2076
+
2077
+ const cachedPath = path.join(cacheDir, moduleId);
2078
+
2079
+ // Check if this is actually a custom module (has module.yaml)
2080
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
2081
+ if (await fs.pathExists(moduleYamlPath)) {
2082
+ // For quick update, we always rebuild from cache
2083
+ customModuleSources.set(moduleId, {
2084
+ id: moduleId,
2085
+ name: moduleId, // We'll read the actual name if needed
2086
+ sourcePath: cachedPath,
2087
+ cached: true, // Flag to indicate this is from cache
2088
+ });
2089
+ }
2090
+ }
2091
+ }
2092
+ }
2093
+
2094
+ // Load saved IDE configurations
2095
+ const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
2096
+
2097
+ // Get available modules (what we have source for)
2098
+ const availableModulesData = await this.moduleManager.listAvailable();
2099
+ const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
2100
+
2101
+ // Add external official modules to available modules
2102
+ // These can always be obtained by cloning from their remote URLs
2103
+ const { ExternalModuleManager } = require('../modules/external-manager');
2104
+ const externalManager = new ExternalModuleManager();
2105
+ const externalModules = await externalManager.listAvailable();
2106
+ for (const externalModule of externalModules) {
2107
+ // Only add if not already in the list and is installed
2108
+ if (installedModules.includes(externalModule.code) && !availableModules.some((m) => m.id === externalModule.code)) {
2109
+ availableModules.push({
2110
+ id: externalModule.code,
2111
+ name: externalModule.name,
2112
+ isExternal: true,
2113
+ fromExternal: true,
2114
+ });
2115
+ }
2116
+ }
2117
+
2118
+ // Add custom modules from manifest if their sources exist
2119
+ for (const [moduleId, customModule] of customModuleSources) {
2120
+ // Use the absolute sourcePath
2121
+ const sourcePath = customModule.sourcePath;
2122
+
2123
+ // Check if source exists at the recorded path
2124
+ if (
2125
+ sourcePath &&
2126
+ (await fs.pathExists(sourcePath)) && // Add to available modules if not already there
2127
+ !availableModules.some((m) => m.id === moduleId)
2128
+ ) {
2129
+ availableModules.push({
2130
+ id: moduleId,
2131
+ name: customModule.name || moduleId,
2132
+ path: sourcePath,
2133
+ isCustom: true,
2134
+ fromManifest: true,
2135
+ });
2136
+ }
2137
+ }
2138
+
2139
+ // Handle missing custom module sources using shared method
2140
+ const customModuleResult = await this.handleMissingCustomSources(
2141
+ customModuleSources,
2142
+ bmadDir,
2143
+ projectRoot,
2144
+ 'update',
2145
+ installedModules,
2146
+ );
2147
+
2148
+ const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
2149
+
2150
+ const customModulesFromManifest = validCustomModules.map((m) => ({
2151
+ ...m,
2152
+ isCustom: true,
2153
+ hasUpdate: true,
2154
+ }));
2155
+
2156
+ const allAvailableModules = [...availableModules, ...customModulesFromManifest];
2157
+ const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
2158
+
2159
+ // Core module is special - never include it in update flow
2160
+ const nonCoreInstalledModules = installedModules.filter((id) => id !== 'core');
2161
+
2162
+ // Only update modules that are BOTH installed AND available (we have source for)
2163
+ const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id));
2164
+ const skippedModules = nonCoreInstalledModules.filter((id) => !availableModuleIds.has(id));
2165
+
2166
+ // Add custom modules that were kept without sources to the skipped modules
2167
+ // This ensures their agents are preserved in the manifest
2168
+ for (const keptModule of keptModulesWithoutSources) {
2169
+ if (!skippedModules.includes(keptModule)) {
2170
+ skippedModules.push(keptModule);
2171
+ }
2172
+ }
2173
+
2174
+ spinner.succeed(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`);
2175
+
2176
+ if (skippedModules.length > 0) {
2177
+ console.log(chalk.yellow(`⚠️ Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`));
2178
+ }
2179
+
2180
+ // Load existing configs and collect new fields (if any)
2181
+ console.log(chalk.cyan('\n📋 Checking for new configuration options...'));
2182
+ await this.configCollector.loadExistingConfig(projectDir);
2183
+
2184
+ let promptedForNewFields = false;
2185
+
2186
+ // Check core config for new fields
2187
+ const corePrompted = await this.configCollector.collectModuleConfigQuick('core', projectDir, true);
2188
+ if (corePrompted) {
2189
+ promptedForNewFields = true;
2190
+ }
2191
+
2192
+ // Check each module we're updating for new fields (NOT skipped modules)
2193
+ for (const moduleName of modulesToUpdate) {
2194
+ const modulePrompted = await this.configCollector.collectModuleConfigQuick(moduleName, projectDir, true);
2195
+ if (modulePrompted) {
2196
+ promptedForNewFields = true;
2197
+ }
2198
+ }
2199
+
2200
+ if (!promptedForNewFields) {
2201
+ console.log(chalk.green('✓ All configuration is up to date, no new options to configure'));
2202
+ }
2203
+
2204
+ // Add metadata
2205
+ this.configCollector.collectedConfig._meta = {
2206
+ version: require(path.join(getProjectRoot(), 'package.json')).version,
2207
+ installDate: new Date().toISOString(),
2208
+ lastModified: new Date().toISOString(),
2209
+ };
2210
+
2211
+ // Build the config object for the installer
2212
+ const installConfig = {
2213
+ directory: projectDir,
2214
+ installCore: true,
2215
+ modules: modulesToUpdate, // Only update modules we have source for
2216
+ ides: configuredIdes,
2217
+ skipIde: configuredIdes.length === 0,
2218
+ coreConfig: this.configCollector.collectedConfig.core,
2219
+ actionType: 'install', // Use regular install flow
2220
+ _quickUpdate: true, // Flag to skip certain prompts
2221
+ _preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
2222
+ _savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer
2223
+ _customModuleSources: customModuleSources, // Pass custom module sources for updates
2224
+ _existingModules: installedModules, // Pass all installed modules for manifest generation
2225
+ };
2226
+
2227
+ // Call the standard install method
2228
+ const result = await this.install(installConfig);
2229
+
2230
+ // Only succeed the spinner if it's still spinning
2231
+ // (install method might have stopped it if folder name changed)
2232
+ if (spinner.isSpinning) {
2233
+ spinner.succeed('Quick update complete!');
2234
+ }
2235
+
2236
+ return {
2237
+ success: true,
2238
+ moduleCount: modulesToUpdate.length + 1, // +1 for core
2239
+ hadNewFields: promptedForNewFields,
2240
+ modules: ['core', ...modulesToUpdate],
2241
+ skippedModules: skippedModules,
2242
+ ides: configuredIdes,
2243
+ };
2244
+ } catch (error) {
2245
+ spinner.fail('Quick update failed');
2246
+ throw error;
2247
+ }
2248
+ }
2249
+
2250
+ /**
2251
+ * Compile agents with customizations only
2252
+ * @param {Object} config - Configuration with directory
2253
+ * @returns {Object} Compilation result
2254
+ */
2255
+ async compileAgents(config) {
2256
+ const ora = require('ora');
2257
+ const chalk = require('chalk');
2258
+ const { ModuleManager } = require('../modules/manager');
2259
+ const { getSourcePath } = require('../../../lib/project-root');
2260
+
2261
+ const spinner = ora('Recompiling agents with customizations...').start();
2262
+
2263
+ try {
2264
+ const projectDir = path.resolve(config.directory);
2265
+ const { bmadDir } = await this.findBmadDir(projectDir);
2266
+
2267
+ // Check if bmad directory exists
2268
+ if (!(await fs.pathExists(bmadDir))) {
2269
+ spinner.fail('No BMAD installation found');
2270
+ throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
2271
+ }
2272
+
2273
+ // Detect existing installation
2274
+ const existingInstall = await this.detector.detect(bmadDir);
2275
+ const installedModules = existingInstall.modules.map((m) => m.id);
2276
+
2277
+ // Initialize module manager
2278
+ const moduleManager = new ModuleManager();
2279
+ moduleManager.setBmadFolderName(path.basename(bmadDir));
2280
+
2281
+ let totalAgentCount = 0;
2282
+
2283
+ // Get custom module sources from cache
2284
+ const customModuleSources = new Map();
2285
+ const cacheDir = path.join(bmadDir, '_config', 'custom');
2286
+ if (await fs.pathExists(cacheDir)) {
2287
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
2288
+
2289
+ for (const cachedModule of cachedModules) {
2290
+ if (cachedModule.isDirectory()) {
2291
+ const moduleId = cachedModule.name;
2292
+ const cachedPath = path.join(cacheDir, moduleId);
2293
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
2294
+
2295
+ // Check if this is actually a custom module
2296
+ if (await fs.pathExists(moduleYamlPath)) {
2297
+ // Check if this is an external official module - skip cache for those
2298
+ const isExternal = await this.moduleManager.isExternalModule(moduleId);
2299
+ if (isExternal) {
2300
+ // External modules are handled via cloneExternalModule, not from cache
2301
+ continue;
2302
+ }
2303
+ customModuleSources.set(moduleId, cachedPath);
2304
+ }
2305
+ }
2306
+ }
2307
+ }
2308
+
2309
+ // Process each installed module
2310
+ for (const moduleId of installedModules) {
2311
+ spinner.text = `Recompiling agents in ${moduleId}...`;
2312
+
2313
+ // Get source path
2314
+ let sourcePath;
2315
+ if (moduleId === 'core') {
2316
+ sourcePath = getSourcePath('core');
2317
+ } else {
2318
+ // First check if it's in the custom cache
2319
+ if (customModuleSources.has(moduleId)) {
2320
+ sourcePath = customModuleSources.get(moduleId);
2321
+ } else {
2322
+ sourcePath = await moduleManager.findModuleSource(moduleId);
2323
+ }
2324
+ }
2325
+
2326
+ if (!sourcePath) {
2327
+ console.log(chalk.yellow(` Warning: Source not found for module ${moduleId}, skipping...`));
2328
+ continue;
2329
+ }
2330
+
2331
+ const targetPath = path.join(bmadDir, moduleId);
2332
+
2333
+ // Compile agents for this module
2334
+ await moduleManager.compileModuleAgents(sourcePath, targetPath, moduleId, bmadDir, this);
2335
+
2336
+ // Count agents (rough estimate based on files)
2337
+ const agentsPath = path.join(targetPath, 'agents');
2338
+ if (await fs.pathExists(agentsPath)) {
2339
+ const agentFiles = await fs.readdir(agentsPath);
2340
+ const agentCount = agentFiles.filter((f) => f.endsWith('.md')).length;
2341
+ totalAgentCount += agentCount;
2342
+ }
2343
+ }
2344
+
2345
+ spinner.succeed('Agent recompilation complete!');
2346
+
2347
+ return {
2348
+ success: true,
2349
+ agentCount: totalAgentCount,
2350
+ modules: installedModules,
2351
+ };
2352
+ } catch (error) {
2353
+ spinner.fail('Agent recompilation failed');
2354
+ throw error;
2355
+ }
2356
+ }
2357
+
2358
+ /**
2359
+ * Private: Prompt for update action
2360
+ */
2361
+ async promptUpdateAction() {
2362
+ const action = await prompts.select({
2363
+ message: 'What would you like to do?',
2364
+ choices: [{ name: 'Update existing installation', value: 'update' }],
2365
+ });
2366
+ return { action };
2367
+ }
2368
+
2369
+ /**
2370
+ * Handle legacy BMAD v4 detection with simple warning
2371
+ * @param {string} _projectDir - Project directory (unused in simplified version)
2372
+ * @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
2373
+ */
2374
+ async handleLegacyV4Migration(_projectDir, _legacyV4) {
2375
+ console.log('');
2376
+ console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
2377
+ console.log(chalk.yellow('─'.repeat(80)));
2378
+ console.log(chalk.yellow('Found .bmad-method folder from BMAD v4 installation.'));
2379
+ console.log('');
2380
+
2381
+ console.log(chalk.dim('Before continuing with installation, we recommend:'));
2382
+ console.log(chalk.dim(' 1. Remove the .bmad-method folder, OR'));
2383
+ console.log(chalk.dim(' 2. Back it up by renaming it to another name (e.g., bmad-method-backup)'));
2384
+ console.log('');
2385
+
2386
+ console.log(chalk.dim('If your v4 installation set up rules or commands, you should remove those as well.'));
2387
+ console.log('');
2388
+
2389
+ const proceed = await prompts.select({
2390
+ message: 'What would you like to do?',
2391
+ choices: [
2392
+ {
2393
+ name: 'Exit and clean up manually (recommended)',
2394
+ value: 'exit',
2395
+ hint: 'Exit installation',
2396
+ },
2397
+ {
2398
+ name: 'Continue with installation anyway',
2399
+ value: 'continue',
2400
+ hint: 'Continue',
2401
+ },
2402
+ ],
2403
+ default: 'exit',
2404
+ });
2405
+
2406
+ if (proceed === 'exit') {
2407
+ console.log('');
2408
+ console.log(chalk.cyan('Please remove the .bmad-method folder and any v4 rules/commands,'));
2409
+ console.log(chalk.cyan('then run the installer again.'));
2410
+ console.log('');
2411
+ process.exit(0);
2412
+ }
2413
+
2414
+ console.log('');
2415
+ console.log(chalk.yellow('⚠️ Proceeding with installation despite legacy v4 folder'));
2416
+ console.log('');
2417
+ }
2418
+
2419
+ /**
2420
+ * Read files-manifest.csv
2421
+ * @param {string} bmadDir - BMAD installation directory
2422
+ * @returns {Array} Array of file entries from files-manifest.csv
2423
+ */
2424
+ async readFilesManifest(bmadDir) {
2425
+ const filesManifestPath = path.join(bmadDir, '_config', 'files-manifest.csv');
2426
+ if (!(await fs.pathExists(filesManifestPath))) {
2427
+ return [];
2428
+ }
2429
+
2430
+ try {
2431
+ const content = await fs.readFile(filesManifestPath, 'utf8');
2432
+ const lines = content.split('\n');
2433
+ const files = [];
2434
+
2435
+ for (let i = 1; i < lines.length; i++) {
2436
+ // Skip header
2437
+ const line = lines[i].trim();
2438
+ if (!line) continue;
2439
+
2440
+ // Parse CSV line properly handling quoted values
2441
+ const parts = [];
2442
+ let current = '';
2443
+ let inQuotes = false;
2444
+
2445
+ for (const char of line) {
2446
+ if (char === '"') {
2447
+ inQuotes = !inQuotes;
2448
+ } else if (char === ',' && !inQuotes) {
2449
+ parts.push(current);
2450
+ current = '';
2451
+ } else {
2452
+ current += char;
2453
+ }
2454
+ }
2455
+ parts.push(current); // Add last part
2456
+
2457
+ if (parts.length >= 4) {
2458
+ files.push({
2459
+ type: parts[0],
2460
+ name: parts[1],
2461
+ module: parts[2],
2462
+ path: parts[3],
2463
+ hash: parts[4] || null, // Hash may not exist in old manifests
2464
+ });
2465
+ }
2466
+ }
2467
+
2468
+ return files;
2469
+ } catch (error) {
2470
+ console.warn('Warning: Could not read files-manifest.csv:', error.message);
2471
+ return [];
2472
+ }
2473
+ }
2474
+
2475
+ /**
2476
+ * Detect custom and modified files
2477
+ * @param {string} bmadDir - BMAD installation directory
2478
+ * @param {Array} existingFilesManifest - Previous files from files-manifest.csv
2479
+ * @returns {Object} Object with customFiles and modifiedFiles arrays
2480
+ */
2481
+ async detectCustomFiles(bmadDir, existingFilesManifest) {
2482
+ const customFiles = [];
2483
+ const modifiedFiles = [];
2484
+
2485
+ // Memory is always in _bmad/_memory
2486
+ const bmadMemoryPath = '_memory';
2487
+
2488
+ // Check if the manifest has hashes - if not, we can't detect modifications
2489
+ let manifestHasHashes = false;
2490
+ if (existingFilesManifest && existingFilesManifest.length > 0) {
2491
+ manifestHasHashes = existingFilesManifest.some((f) => f.hash);
2492
+ }
2493
+
2494
+ // Build map of previously installed files from files-manifest.csv with their hashes
2495
+ const installedFilesMap = new Map();
2496
+ for (const fileEntry of existingFilesManifest) {
2497
+ if (fileEntry.path) {
2498
+ const absolutePath = path.join(bmadDir, fileEntry.path);
2499
+ installedFilesMap.set(path.normalize(absolutePath), {
2500
+ hash: fileEntry.hash,
2501
+ relativePath: fileEntry.path,
2502
+ });
2503
+ }
2504
+ }
2505
+
2506
+ // Recursively scan bmadDir for all files
2507
+ const scanDirectory = async (dir) => {
2508
+ try {
2509
+ const entries = await fs.readdir(dir, { withFileTypes: true });
2510
+ for (const entry of entries) {
2511
+ const fullPath = path.join(dir, entry.name);
2512
+
2513
+ if (entry.isDirectory()) {
2514
+ // Skip certain directories
2515
+ if (entry.name === 'node_modules' || entry.name === '.git') {
2516
+ continue;
2517
+ }
2518
+ await scanDirectory(fullPath);
2519
+ } else if (entry.isFile()) {
2520
+ const normalizedPath = path.normalize(fullPath);
2521
+ const fileInfo = installedFilesMap.get(normalizedPath);
2522
+
2523
+ // Skip certain system files that are auto-generated
2524
+ const relativePath = path.relative(bmadDir, fullPath);
2525
+ const fileName = path.basename(fullPath);
2526
+
2527
+ // Skip _config directory EXCEPT for modified agent customizations
2528
+ if (relativePath.startsWith('_config/') || relativePath.startsWith('_config\\')) {
2529
+ // Special handling for .customize.yaml files - only preserve if modified
2530
+ if (relativePath.includes('/agents/') && fileName.endsWith('.customize.yaml')) {
2531
+ // Check if the customization file has been modified from manifest
2532
+ const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
2533
+ if (await fs.pathExists(manifestPath)) {
2534
+ const crypto = require('node:crypto');
2535
+ const currentContent = await fs.readFile(fullPath, 'utf8');
2536
+ const currentHash = crypto.createHash('sha256').update(currentContent).digest('hex');
2537
+
2538
+ const yaml = require('yaml');
2539
+ const manifestContent = await fs.readFile(manifestPath, 'utf8');
2540
+ const manifestData = yaml.parse(manifestContent);
2541
+ const originalHash = manifestData.agentCustomizations?.[relativePath];
2542
+
2543
+ // Only add to customFiles if hash differs (user modified)
2544
+ if (originalHash && currentHash !== originalHash) {
2545
+ customFiles.push(fullPath);
2546
+ }
2547
+ }
2548
+ }
2549
+ continue;
2550
+ }
2551
+
2552
+ if (relativePath.startsWith(bmadMemoryPath + '/') && path.dirname(relativePath).includes('-sidecar')) {
2553
+ continue;
2554
+ }
2555
+
2556
+ // Skip config.yaml files - these are regenerated on each install/update
2557
+ if (fileName === 'config.yaml') {
2558
+ continue;
2559
+ }
2560
+
2561
+ if (!fileInfo) {
2562
+ // File not in manifest = custom file
2563
+ // EXCEPT: Agent .md files in module folders are generated files, not custom
2564
+ // Only treat .md files under _config/agents/ as custom
2565
+ if (!(fileName.endsWith('.md') && relativePath.includes('/agents/') && !relativePath.startsWith('_config/'))) {
2566
+ customFiles.push(fullPath);
2567
+ }
2568
+ } else if (manifestHasHashes && fileInfo.hash) {
2569
+ // File in manifest with hash - check if it was modified
2570
+ const currentHash = await this.manifest.calculateFileHash(fullPath);
2571
+ if (currentHash && currentHash !== fileInfo.hash) {
2572
+ // Hash changed = file was modified
2573
+ modifiedFiles.push({
2574
+ path: fullPath,
2575
+ relativePath: fileInfo.relativePath,
2576
+ });
2577
+ }
2578
+ }
2579
+ }
2580
+ }
2581
+ } catch {
2582
+ // Ignore errors scanning directories
2583
+ }
2584
+ };
2585
+
2586
+ await scanDirectory(bmadDir);
2587
+ return { customFiles, modifiedFiles };
2588
+ }
2589
+
2590
+ /**
2591
+ * Handle missing custom module sources interactively
2592
+ * @param {Map} customModuleSources - Map of custom module ID to info
2593
+ * @param {string} bmadDir - BMAD directory
2594
+ * @param {string} projectRoot - Project root directory
2595
+ * @param {string} operation - Current operation ('update', 'compile', etc.)
2596
+ * @param {Array} installedModules - Array of installed module IDs (will be modified)
2597
+ * @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
2598
+ */
2599
+ async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules) {
2600
+ const validCustomModules = [];
2601
+ const keptModulesWithoutSources = []; // Track modules kept without sources
2602
+ const customModulesWithMissingSources = [];
2603
+
2604
+ // Check which sources exist
2605
+ for (const [moduleId, customInfo] of customModuleSources) {
2606
+ if (await fs.pathExists(customInfo.sourcePath)) {
2607
+ validCustomModules.push({
2608
+ id: moduleId,
2609
+ name: customInfo.name,
2610
+ path: customInfo.sourcePath,
2611
+ info: customInfo,
2612
+ });
2613
+ } else {
2614
+ // For cached modules that are missing, we just skip them without prompting
2615
+ if (customInfo.cached) {
2616
+ // Skip cached modules without prompting
2617
+ keptModulesWithoutSources.push({
2618
+ id: moduleId,
2619
+ name: customInfo.name,
2620
+ cached: true,
2621
+ });
2622
+ } else {
2623
+ customModulesWithMissingSources.push({
2624
+ id: moduleId,
2625
+ name: customInfo.name,
2626
+ sourcePath: customInfo.sourcePath,
2627
+ relativePath: customInfo.relativePath,
2628
+ info: customInfo,
2629
+ });
2630
+ }
2631
+ }
2632
+ }
2633
+
2634
+ // If no missing sources, return immediately
2635
+ if (customModulesWithMissingSources.length === 0) {
2636
+ return {
2637
+ validCustomModules,
2638
+ keptModulesWithoutSources: [],
2639
+ };
2640
+ }
2641
+
2642
+ // Stop any spinner for interactive prompts
2643
+ const currentSpinner = ora();
2644
+ if (currentSpinner.isSpinning) {
2645
+ currentSpinner.stop();
2646
+ }
2647
+
2648
+ console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
2649
+
2650
+ let keptCount = 0;
2651
+ let updatedCount = 0;
2652
+ let removedCount = 0;
2653
+
2654
+ for (const missing of customModulesWithMissingSources) {
2655
+ console.log(chalk.dim(` • ${missing.name} (${missing.id})`));
2656
+ console.log(chalk.dim(` Original source: ${missing.relativePath}`));
2657
+ console.log(chalk.dim(` Full path: ${missing.sourcePath}`));
2658
+
2659
+ const choices = [
2660
+ {
2661
+ name: 'Keep installed (will not be processed)',
2662
+ value: 'keep',
2663
+ hint: 'Keep',
2664
+ },
2665
+ {
2666
+ name: 'Specify new source location',
2667
+ value: 'update',
2668
+ hint: 'Update',
2669
+ },
2670
+ ];
2671
+
2672
+ // Only add remove option if not just compiling agents
2673
+ if (operation !== 'compile-agents') {
2674
+ choices.push({
2675
+ name: '⚠️ REMOVE module completely (destructive!)',
2676
+ value: 'remove',
2677
+ hint: 'Remove',
2678
+ });
2679
+ }
2680
+
2681
+ const action = await prompts.select({
2682
+ message: `How would you like to handle "${missing.name}"?`,
2683
+ choices,
2684
+ });
2685
+
2686
+ switch (action) {
2687
+ case 'update': {
2688
+ // Use sync validation because @clack/prompts doesn't support async validate
2689
+ const newSourcePath = await prompts.text({
2690
+ message: 'Enter the new path to the custom module:',
2691
+ default: missing.sourcePath,
2692
+ validate: (input) => {
2693
+ if (!input || input.trim() === '') {
2694
+ return 'Please enter a path';
2695
+ }
2696
+ const expandedPath = path.resolve(input.trim());
2697
+ if (!fs.pathExistsSync(expandedPath)) {
2698
+ return 'Path does not exist';
2699
+ }
2700
+ // Check if it looks like a valid module
2701
+ const moduleYamlPath = path.join(expandedPath, 'module.yaml');
2702
+ const agentsPath = path.join(expandedPath, 'agents');
2703
+ const workflowsPath = path.join(expandedPath, 'workflows');
2704
+
2705
+ if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
2706
+ return 'Path does not appear to contain a valid custom module';
2707
+ }
2708
+ return; // clack expects undefined for valid input
2709
+ },
2710
+ });
2711
+
2712
+ // Update the source in manifest
2713
+ const resolvedPath = path.resolve(newSourcePath.trim());
2714
+ missing.info.sourcePath = resolvedPath;
2715
+ // Remove relativePath - we only store absolute sourcePath now
2716
+ delete missing.info.relativePath;
2717
+ await this.manifest.addCustomModule(bmadDir, missing.info);
2718
+
2719
+ validCustomModules.push({
2720
+ id: missing.id,
2721
+ name: missing.name,
2722
+ path: resolvedPath,
2723
+ info: missing.info,
2724
+ });
2725
+
2726
+ updatedCount++;
2727
+ console.log(chalk.green(`✓ Updated source location`));
2728
+
2729
+ break;
2730
+ }
2731
+ case 'remove': {
2732
+ // Extra confirmation for destructive remove
2733
+ console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
2734
+ console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
2735
+
2736
+ const confirmDelete = await prompts.confirm({
2737
+ message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
2738
+ default: false,
2739
+ });
2740
+
2741
+ if (confirmDelete) {
2742
+ const typedConfirm = await prompts.text({
2743
+ message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
2744
+ validate: (input) => {
2745
+ if (input !== 'DELETE') {
2746
+ return chalk.red('You must type "DELETE" exactly to proceed');
2747
+ }
2748
+ return; // clack expects undefined for valid input
2749
+ },
2750
+ });
2751
+
2752
+ if (typedConfirm === 'DELETE') {
2753
+ // Remove the module from filesystem and manifest
2754
+ const modulePath = path.join(bmadDir, missing.id);
2755
+ if (await fs.pathExists(modulePath)) {
2756
+ const fsExtra = require('fs-extra');
2757
+ await fsExtra.remove(modulePath);
2758
+ console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
2759
+ }
2760
+
2761
+ await this.manifest.removeModule(bmadDir, missing.id);
2762
+ await this.manifest.removeCustomModule(bmadDir, missing.id);
2763
+ console.log(chalk.yellow(` ✓ Removed from manifest`));
2764
+
2765
+ // Also remove from installedModules list
2766
+ if (installedModules && installedModules.includes(missing.id)) {
2767
+ const index = installedModules.indexOf(missing.id);
2768
+ if (index !== -1) {
2769
+ installedModules.splice(index, 1);
2770
+ }
2771
+ }
2772
+
2773
+ removedCount++;
2774
+ console.log(chalk.red.bold(`✓ "${missing.name}" has been permanently removed`));
2775
+ } else {
2776
+ console.log(chalk.dim(' Removal cancelled - module will be kept'));
2777
+ keptCount++;
2778
+ }
2779
+ } else {
2780
+ console.log(chalk.dim(' Removal cancelled - module will be kept'));
2781
+ keptCount++;
2782
+ }
2783
+
2784
+ break;
2785
+ }
2786
+ case 'keep': {
2787
+ keptCount++;
2788
+ keptModulesWithoutSources.push(missing.id);
2789
+ console.log(chalk.dim(` Module will be kept as-is`));
2790
+
2791
+ break;
2792
+ }
2793
+ // No default
2794
+ }
2795
+ }
2796
+
2797
+ // Show summary
2798
+ if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
2799
+ console.log(chalk.dim(`\nSummary for custom modules with missing sources:`));
2800
+ if (keptCount > 0) console.log(chalk.dim(` • ${keptCount} module(s) kept as-is`));
2801
+ if (updatedCount > 0) console.log(chalk.dim(` • ${updatedCount} module(s) updated with new sources`));
2802
+ if (removedCount > 0) console.log(chalk.red(` • ${removedCount} module(s) permanently deleted`));
2803
+ }
2804
+
2805
+ return {
2806
+ validCustomModules,
2807
+ keptModulesWithoutSources,
2808
+ };
2809
+ }
2810
+ }
2811
+
2812
+ module.exports = { Installer };