scm-method 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 (324) hide show
  1. package/.claude-plugin/marketplace.json +77 -0
  2. package/AGENTS.md +12 -0
  3. package/LICENSE +30 -0
  4. package/README.md +109 -0
  5. package/README_CN.md +108 -0
  6. package/package.json +110 -0
  7. package/src/bmm-skills/1-analysis/research/scm-domain-research/SKILL.md +6 -0
  8. package/src/bmm-skills/1-analysis/research/scm-domain-research/domain-steps/step-01-init.md +137 -0
  9. package/src/bmm-skills/1-analysis/research/scm-domain-research/domain-steps/step-02-domain-analysis.md +229 -0
  10. package/src/bmm-skills/1-analysis/research/scm-domain-research/domain-steps/step-03-competitive-landscape.md +238 -0
  11. package/src/bmm-skills/1-analysis/research/scm-domain-research/domain-steps/step-04-regulatory-focus.md +206 -0
  12. package/src/bmm-skills/1-analysis/research/scm-domain-research/domain-steps/step-05-technical-trends.md +234 -0
  13. package/src/bmm-skills/1-analysis/research/scm-domain-research/domain-steps/step-06-research-synthesis.md +444 -0
  14. package/src/bmm-skills/1-analysis/research/scm-domain-research/research.template.md +29 -0
  15. package/src/bmm-skills/1-analysis/research/scm-domain-research/workflow.md +51 -0
  16. package/src/bmm-skills/1-analysis/research/scm-market-research/SKILL.md +6 -0
  17. package/src/bmm-skills/1-analysis/research/scm-market-research/research.template.md +29 -0
  18. package/src/bmm-skills/1-analysis/research/scm-market-research/steps/step-01-init.md +184 -0
  19. package/src/bmm-skills/1-analysis/research/scm-market-research/steps/step-02-customer-behavior.md +239 -0
  20. package/src/bmm-skills/1-analysis/research/scm-market-research/steps/step-03-customer-pain-points.md +251 -0
  21. package/src/bmm-skills/1-analysis/research/scm-market-research/steps/step-04-customer-decisions.md +261 -0
  22. package/src/bmm-skills/1-analysis/research/scm-market-research/steps/step-05-competitive-analysis.md +173 -0
  23. package/src/bmm-skills/1-analysis/research/scm-market-research/steps/step-06-research-completion.md +478 -0
  24. package/src/bmm-skills/1-analysis/research/scm-market-research/workflow.md +51 -0
  25. package/src/bmm-skills/1-analysis/research/scm-technical-research/SKILL.md +6 -0
  26. package/src/bmm-skills/1-analysis/research/scm-technical-research/research.template.md +29 -0
  27. package/src/bmm-skills/1-analysis/research/scm-technical-research/technical-steps/step-01-init.md +137 -0
  28. package/src/bmm-skills/1-analysis/research/scm-technical-research/technical-steps/step-02-technical-overview.md +239 -0
  29. package/src/bmm-skills/1-analysis/research/scm-technical-research/technical-steps/step-03-integration-patterns.md +248 -0
  30. package/src/bmm-skills/1-analysis/research/scm-technical-research/technical-steps/step-04-architectural-patterns.md +202 -0
  31. package/src/bmm-skills/1-analysis/research/scm-technical-research/technical-steps/step-05-implementation-research.md +233 -0
  32. package/src/bmm-skills/1-analysis/research/scm-technical-research/technical-steps/step-06-research-synthesis.md +487 -0
  33. package/src/bmm-skills/1-analysis/research/scm-technical-research/workflow.md +52 -0
  34. package/src/bmm-skills/1-analysis/scm-agent-analyst/SKILL.md +59 -0
  35. package/src/bmm-skills/1-analysis/scm-agent-analyst/scm-skill-manifest.yaml +11 -0
  36. package/src/bmm-skills/1-analysis/scm-agent-tech-writer/SKILL.md +57 -0
  37. package/src/bmm-skills/1-analysis/scm-agent-tech-writer/explain-concept.md +20 -0
  38. package/src/bmm-skills/1-analysis/scm-agent-tech-writer/mermaid-gen.md +20 -0
  39. package/src/bmm-skills/1-analysis/scm-agent-tech-writer/scm-skill-manifest.yaml +11 -0
  40. package/src/bmm-skills/1-analysis/scm-agent-tech-writer/validate-doc.md +19 -0
  41. package/src/bmm-skills/1-analysis/scm-agent-tech-writer/write-document.md +20 -0
  42. package/src/bmm-skills/1-analysis/scm-document-project/SKILL.md +6 -0
  43. package/src/bmm-skills/1-analysis/scm-document-project/checklist.md +245 -0
  44. package/src/bmm-skills/1-analysis/scm-document-project/documentation-requirements.csv +12 -0
  45. package/src/bmm-skills/1-analysis/scm-document-project/instructions.md +128 -0
  46. package/src/bmm-skills/1-analysis/scm-document-project/templates/deep-dive-template.md +345 -0
  47. package/src/bmm-skills/1-analysis/scm-document-project/templates/index-template.md +169 -0
  48. package/src/bmm-skills/1-analysis/scm-document-project/templates/project-overview-template.md +103 -0
  49. package/src/bmm-skills/1-analysis/scm-document-project/templates/project-scan-report-schema.json +160 -0
  50. package/src/bmm-skills/1-analysis/scm-document-project/templates/source-tree-template.md +135 -0
  51. package/src/bmm-skills/1-analysis/scm-document-project/workflow.md +25 -0
  52. package/src/bmm-skills/1-analysis/scm-document-project/workflows/deep-dive-instructions.md +299 -0
  53. package/src/bmm-skills/1-analysis/scm-document-project/workflows/deep-dive-workflow.md +34 -0
  54. package/src/bmm-skills/1-analysis/scm-document-project/workflows/full-scan-instructions.md +1107 -0
  55. package/src/bmm-skills/1-analysis/scm-document-project/workflows/full-scan-workflow.md +34 -0
  56. package/src/bmm-skills/1-analysis/scm-prfaq/SKILL.md +96 -0
  57. package/src/bmm-skills/1-analysis/scm-prfaq/agents/artifact-analyzer.md +60 -0
  58. package/src/bmm-skills/1-analysis/scm-prfaq/agents/web-researcher.md +49 -0
  59. package/src/bmm-skills/1-analysis/scm-prfaq/assets/prfaq-template.md +62 -0
  60. package/src/bmm-skills/1-analysis/scm-prfaq/references/customer-faq.md +55 -0
  61. package/src/bmm-skills/1-analysis/scm-prfaq/references/internal-faq.md +51 -0
  62. package/src/bmm-skills/1-analysis/scm-prfaq/references/press-release.md +60 -0
  63. package/src/bmm-skills/1-analysis/scm-prfaq/references/verdict.md +79 -0
  64. package/src/bmm-skills/1-analysis/scm-prfaq/scm-manifest.json +16 -0
  65. package/src/bmm-skills/1-analysis/scm-product-brief/SKILL.md +82 -0
  66. package/src/bmm-skills/1-analysis/scm-product-brief/agents/artifact-analyzer.md +60 -0
  67. package/src/bmm-skills/1-analysis/scm-product-brief/agents/opportunity-reviewer.md +44 -0
  68. package/src/bmm-skills/1-analysis/scm-product-brief/agents/skeptic-reviewer.md +44 -0
  69. package/src/bmm-skills/1-analysis/scm-product-brief/agents/web-researcher.md +49 -0
  70. package/src/bmm-skills/1-analysis/scm-product-brief/prompts/contextual-discovery.md +57 -0
  71. package/src/bmm-skills/1-analysis/scm-product-brief/prompts/draft-and-review.md +86 -0
  72. package/src/bmm-skills/1-analysis/scm-product-brief/prompts/finalize.md +75 -0
  73. package/src/bmm-skills/1-analysis/scm-product-brief/prompts/guided-elicitation.md +70 -0
  74. package/src/bmm-skills/1-analysis/scm-product-brief/resources/brief-template.md +60 -0
  75. package/src/bmm-skills/1-analysis/scm-product-brief/scm-manifest.json +17 -0
  76. package/src/bmm-skills/2-plan-workflows/scm-agent-pm/SKILL.md +59 -0
  77. package/src/bmm-skills/2-plan-workflows/scm-agent-pm/scm-skill-manifest.yaml +11 -0
  78. package/src/bmm-skills/2-plan-workflows/scm-agent-ux-designer/SKILL.md +55 -0
  79. package/src/bmm-skills/2-plan-workflows/scm-agent-ux-designer/scm-skill-manifest.yaml +11 -0
  80. package/src/bmm-skills/2-plan-workflows/scm-create-prd/SKILL.md +6 -0
  81. package/src/bmm-skills/2-plan-workflows/scm-create-prd/data/domain-complexity.csv +15 -0
  82. package/src/bmm-skills/2-plan-workflows/scm-create-prd/data/prd-purpose.md +197 -0
  83. package/src/bmm-skills/2-plan-workflows/scm-create-prd/data/project-types.csv +11 -0
  84. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-01-init.md +178 -0
  85. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-01b-continue.md +161 -0
  86. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-02-discovery.md +208 -0
  87. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-02b-vision.md +142 -0
  88. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-02c-executive-summary.md +158 -0
  89. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-03-success.md +214 -0
  90. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-04-journeys.md +201 -0
  91. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-05-domain.md +194 -0
  92. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-06-innovation.md +211 -0
  93. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-07-project-type.md +222 -0
  94. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-08-scoping.md +216 -0
  95. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-09-functional.md +219 -0
  96. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-10-nonfunctional.md +230 -0
  97. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-11-polish.md +221 -0
  98. package/src/bmm-skills/2-plan-workflows/scm-create-prd/steps-c/step-12-complete.md +115 -0
  99. package/src/bmm-skills/2-plan-workflows/scm-create-prd/templates/prd-template.md +10 -0
  100. package/src/bmm-skills/2-plan-workflows/scm-create-prd/workflow.md +61 -0
  101. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/SKILL.md +6 -0
  102. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-01-init.md +135 -0
  103. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-01b-continue.md +127 -0
  104. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-02-discovery.md +190 -0
  105. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-03-core-experience.md +217 -0
  106. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-04-emotional-response.md +220 -0
  107. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-05-inspiration.md +235 -0
  108. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-06-design-system.md +253 -0
  109. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-07-defining-experience.md +255 -0
  110. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-08-visual-foundation.md +225 -0
  111. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-09-design-directions.md +225 -0
  112. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-10-user-journeys.md +242 -0
  113. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-11-component-strategy.md +249 -0
  114. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-12-ux-patterns.md +238 -0
  115. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-13-responsive-accessibility.md +265 -0
  116. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/steps/step-14-complete.md +171 -0
  117. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/ux-design-template.md +13 -0
  118. package/src/bmm-skills/2-plan-workflows/scm-create-ux-design/workflow.md +35 -0
  119. package/src/bmm-skills/2-plan-workflows/scm-edit-prd/SKILL.md +6 -0
  120. package/src/bmm-skills/2-plan-workflows/scm-edit-prd/steps-e/step-e-01-discovery.md +242 -0
  121. package/src/bmm-skills/2-plan-workflows/scm-edit-prd/steps-e/step-e-01b-legacy-conversion.md +204 -0
  122. package/src/bmm-skills/2-plan-workflows/scm-edit-prd/steps-e/step-e-02-review.md +245 -0
  123. package/src/bmm-skills/2-plan-workflows/scm-edit-prd/steps-e/step-e-03-edit.md +250 -0
  124. package/src/bmm-skills/2-plan-workflows/scm-edit-prd/steps-e/step-e-04-complete.md +165 -0
  125. package/src/bmm-skills/2-plan-workflows/scm-edit-prd/workflow.md +62 -0
  126. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/SKILL.md +6 -0
  127. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/data/domain-complexity.csv +15 -0
  128. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/data/prd-purpose.md +197 -0
  129. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/data/project-types.csv +11 -0
  130. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-01-discovery.md +221 -0
  131. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-02-format-detection.md +188 -0
  132. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-02b-parity-check.md +206 -0
  133. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-03-density-validation.md +171 -0
  134. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-04-brief-coverage-validation.md +211 -0
  135. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-05-measurability-validation.md +225 -0
  136. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-06-traceability-validation.md +214 -0
  137. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-07-implementation-leakage-validation.md +202 -0
  138. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-08-domain-compliance-validation.md +240 -0
  139. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-09-project-type-validation.md +260 -0
  140. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-10-smart-validation.md +206 -0
  141. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-11-holistic-quality-validation.md +261 -0
  142. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-12-completeness-validation.md +239 -0
  143. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/steps-v/step-v-13-report-complete.md +229 -0
  144. package/src/bmm-skills/2-plan-workflows/scm-validate-prd/workflow.md +61 -0
  145. package/src/bmm-skills/3-solutioning/scm-agent-architect/SKILL.md +54 -0
  146. package/src/bmm-skills/3-solutioning/scm-agent-architect/scm-skill-manifest.yaml +11 -0
  147. package/src/bmm-skills/3-solutioning/scm-check-implementation-readiness/SKILL.md +6 -0
  148. package/src/bmm-skills/3-solutioning/scm-check-implementation-readiness/steps/step-01-document-discovery.md +179 -0
  149. package/src/bmm-skills/3-solutioning/scm-check-implementation-readiness/steps/step-02-prd-analysis.md +168 -0
  150. package/src/bmm-skills/3-solutioning/scm-check-implementation-readiness/steps/step-03-epic-coverage-validation.md +169 -0
  151. package/src/bmm-skills/3-solutioning/scm-check-implementation-readiness/steps/step-04-ux-alignment.md +129 -0
  152. package/src/bmm-skills/3-solutioning/scm-check-implementation-readiness/steps/step-05-epic-quality-review.md +241 -0
  153. package/src/bmm-skills/3-solutioning/scm-check-implementation-readiness/steps/step-06-final-assessment.md +126 -0
  154. package/src/bmm-skills/3-solutioning/scm-check-implementation-readiness/templates/readiness-report-template.md +4 -0
  155. package/src/bmm-skills/3-solutioning/scm-check-implementation-readiness/workflow.md +47 -0
  156. package/src/bmm-skills/3-solutioning/scm-create-architecture/SKILL.md +6 -0
  157. package/src/bmm-skills/3-solutioning/scm-create-architecture/architecture-decision-template.md +12 -0
  158. package/src/bmm-skills/3-solutioning/scm-create-architecture/data/domain-complexity.csv +13 -0
  159. package/src/bmm-skills/3-solutioning/scm-create-architecture/data/project-types.csv +7 -0
  160. package/src/bmm-skills/3-solutioning/scm-create-architecture/steps/step-01-init.md +153 -0
  161. package/src/bmm-skills/3-solutioning/scm-create-architecture/steps/step-01b-continue.md +173 -0
  162. package/src/bmm-skills/3-solutioning/scm-create-architecture/steps/step-02-context.md +224 -0
  163. package/src/bmm-skills/3-solutioning/scm-create-architecture/steps/step-03-starter.md +329 -0
  164. package/src/bmm-skills/3-solutioning/scm-create-architecture/steps/step-04-decisions.md +318 -0
  165. package/src/bmm-skills/3-solutioning/scm-create-architecture/steps/step-05-patterns.md +359 -0
  166. package/src/bmm-skills/3-solutioning/scm-create-architecture/steps/step-06-structure.md +379 -0
  167. package/src/bmm-skills/3-solutioning/scm-create-architecture/steps/step-07-validation.md +359 -0
  168. package/src/bmm-skills/3-solutioning/scm-create-architecture/steps/step-08-complete.md +76 -0
  169. package/src/bmm-skills/3-solutioning/scm-create-architecture/workflow.md +32 -0
  170. package/src/bmm-skills/3-solutioning/scm-create-epics-and-stories/SKILL.md +6 -0
  171. package/src/bmm-skills/3-solutioning/scm-create-epics-and-stories/steps/step-01-validate-prerequisites.md +255 -0
  172. package/src/bmm-skills/3-solutioning/scm-create-epics-and-stories/steps/step-02-design-epics.md +212 -0
  173. package/src/bmm-skills/3-solutioning/scm-create-epics-and-stories/steps/step-03-create-stories.md +255 -0
  174. package/src/bmm-skills/3-solutioning/scm-create-epics-and-stories/steps/step-04-final-validation.md +131 -0
  175. package/src/bmm-skills/3-solutioning/scm-create-epics-and-stories/templates/epics-template.md +61 -0
  176. package/src/bmm-skills/3-solutioning/scm-create-epics-and-stories/workflow.md +51 -0
  177. package/src/bmm-skills/3-solutioning/scm-generate-project-context/SKILL.md +6 -0
  178. package/src/bmm-skills/3-solutioning/scm-generate-project-context/project-context-template.md +21 -0
  179. package/src/bmm-skills/3-solutioning/scm-generate-project-context/steps/step-01-discover.md +186 -0
  180. package/src/bmm-skills/3-solutioning/scm-generate-project-context/steps/step-02-generate.md +321 -0
  181. package/src/bmm-skills/3-solutioning/scm-generate-project-context/steps/step-03-complete.md +278 -0
  182. package/src/bmm-skills/3-solutioning/scm-generate-project-context/workflow.md +39 -0
  183. package/src/bmm-skills/4-implementation/scm-agent-dev/SKILL.md +64 -0
  184. package/src/bmm-skills/4-implementation/scm-agent-dev/scm-skill-manifest.yaml +11 -0
  185. package/src/bmm-skills/4-implementation/scm-agent-qa/SKILL.md +61 -0
  186. package/src/bmm-skills/4-implementation/scm-agent-qa/scm-skill-manifest.yaml +11 -0
  187. package/src/bmm-skills/4-implementation/scm-agent-quick-flow-solo-dev/SKILL.md +53 -0
  188. package/src/bmm-skills/4-implementation/scm-agent-quick-flow-solo-dev/scm-skill-manifest.yaml +11 -0
  189. package/src/bmm-skills/4-implementation/scm-agent-sm/SKILL.md +55 -0
  190. package/src/bmm-skills/4-implementation/scm-agent-sm/scm-skill-manifest.yaml +11 -0
  191. package/src/bmm-skills/4-implementation/scm-code-review/SKILL.md +6 -0
  192. package/src/bmm-skills/4-implementation/scm-code-review/steps/step-01-gather-context.md +62 -0
  193. package/src/bmm-skills/4-implementation/scm-code-review/steps/step-02-review.md +34 -0
  194. package/src/bmm-skills/4-implementation/scm-code-review/steps/step-03-triage.md +49 -0
  195. package/src/bmm-skills/4-implementation/scm-code-review/steps/step-04-present.md +129 -0
  196. package/src/bmm-skills/4-implementation/scm-code-review/workflow.md +55 -0
  197. package/src/bmm-skills/4-implementation/scm-correct-course/SKILL.md +6 -0
  198. package/src/bmm-skills/4-implementation/scm-correct-course/checklist.md +288 -0
  199. package/src/bmm-skills/4-implementation/scm-correct-course/workflow.md +267 -0
  200. package/src/bmm-skills/4-implementation/scm-create-story/SKILL.md +6 -0
  201. package/src/bmm-skills/4-implementation/scm-create-story/checklist.md +357 -0
  202. package/src/bmm-skills/4-implementation/scm-create-story/discover-inputs.md +88 -0
  203. package/src/bmm-skills/4-implementation/scm-create-story/template.md +49 -0
  204. package/src/bmm-skills/4-implementation/scm-create-story/workflow.md +380 -0
  205. package/src/bmm-skills/4-implementation/scm-dev-story/SKILL.md +6 -0
  206. package/src/bmm-skills/4-implementation/scm-dev-story/checklist.md +80 -0
  207. package/src/bmm-skills/4-implementation/scm-dev-story/workflow.md +450 -0
  208. package/src/bmm-skills/4-implementation/scm-qa-generate-e2e-tests/SKILL.md +6 -0
  209. package/src/bmm-skills/4-implementation/scm-qa-generate-e2e-tests/checklist.md +33 -0
  210. package/src/bmm-skills/4-implementation/scm-qa-generate-e2e-tests/workflow.md +136 -0
  211. package/src/bmm-skills/4-implementation/scm-quick-dev/SKILL.md +6 -0
  212. package/src/bmm-skills/4-implementation/scm-quick-dev/spec-template.md +88 -0
  213. package/src/bmm-skills/4-implementation/scm-quick-dev/step-01-clarify-and-route.md +66 -0
  214. package/src/bmm-skills/4-implementation/scm-quick-dev/step-02-plan.md +35 -0
  215. package/src/bmm-skills/4-implementation/scm-quick-dev/step-03-implement.md +37 -0
  216. package/src/bmm-skills/4-implementation/scm-quick-dev/step-04-review.md +49 -0
  217. package/src/bmm-skills/4-implementation/scm-quick-dev/step-05-present.md +63 -0
  218. package/src/bmm-skills/4-implementation/scm-quick-dev/step-oneshot.md +62 -0
  219. package/src/bmm-skills/4-implementation/scm-quick-dev/workflow.md +79 -0
  220. package/src/bmm-skills/4-implementation/scm-retrospective/SKILL.md +6 -0
  221. package/src/bmm-skills/4-implementation/scm-retrospective/workflow.md +1479 -0
  222. package/src/bmm-skills/4-implementation/scm-sprint-planning/SKILL.md +6 -0
  223. package/src/bmm-skills/4-implementation/scm-sprint-planning/checklist.md +33 -0
  224. package/src/bmm-skills/4-implementation/scm-sprint-planning/sprint-status-template.yaml +56 -0
  225. package/src/bmm-skills/4-implementation/scm-sprint-planning/workflow.md +263 -0
  226. package/src/bmm-skills/4-implementation/scm-sprint-status/SKILL.md +6 -0
  227. package/src/bmm-skills/4-implementation/scm-sprint-status/workflow.md +261 -0
  228. package/src/bmm-skills/module-help.csv +31 -0
  229. package/src/bmm-skills/module.yaml +50 -0
  230. package/src/core-skills/module-help.csv +11 -0
  231. package/src/core-skills/module.yaml +25 -0
  232. package/src/core-skills/scm-advanced-elicitation/SKILL.md +136 -0
  233. package/src/core-skills/scm-advanced-elicitation/methods.csv +51 -0
  234. package/src/core-skills/scm-brainstorming/SKILL.md +6 -0
  235. package/src/core-skills/scm-brainstorming/brain-methods.csv +62 -0
  236. package/src/core-skills/scm-brainstorming/steps/step-01-session-setup.md +214 -0
  237. package/src/core-skills/scm-brainstorming/steps/step-01b-continue.md +124 -0
  238. package/src/core-skills/scm-brainstorming/steps/step-02a-user-selected.md +229 -0
  239. package/src/core-skills/scm-brainstorming/steps/step-02b-ai-recommended.md +239 -0
  240. package/src/core-skills/scm-brainstorming/steps/step-02c-random-selection.md +211 -0
  241. package/src/core-skills/scm-brainstorming/steps/step-02d-progressive-flow.md +266 -0
  242. package/src/core-skills/scm-brainstorming/steps/step-03-technique-execution.md +401 -0
  243. package/src/core-skills/scm-brainstorming/steps/step-04-idea-organization.md +305 -0
  244. package/src/core-skills/scm-brainstorming/template.md +15 -0
  245. package/src/core-skills/scm-brainstorming/workflow.md +53 -0
  246. package/src/core-skills/scm-distillator/SKILL.md +177 -0
  247. package/src/core-skills/scm-distillator/agents/distillate-compressor.md +116 -0
  248. package/src/core-skills/scm-distillator/agents/round-trip-reconstructor.md +68 -0
  249. package/src/core-skills/scm-distillator/resources/compression-rules.md +51 -0
  250. package/src/core-skills/scm-distillator/resources/distillate-format-reference.md +227 -0
  251. package/src/core-skills/scm-distillator/resources/splitting-strategy.md +78 -0
  252. package/src/core-skills/scm-distillator/scripts/analyze_sources.py +300 -0
  253. package/src/core-skills/scm-distillator/scripts/tests/test_analyze_sources.py +204 -0
  254. package/src/core-skills/scm-editorial-review-prose/SKILL.md +86 -0
  255. package/src/core-skills/scm-editorial-review-structure/SKILL.md +179 -0
  256. package/src/core-skills/scm-help/SKILL.md +73 -0
  257. package/src/core-skills/scm-index-docs/SKILL.md +66 -0
  258. package/src/core-skills/scm-party-mode/SKILL.md +125 -0
  259. package/src/core-skills/scm-review-adversarial-general/SKILL.md +37 -0
  260. package/src/core-skills/scm-review-edge-case-hunter/SKILL.md +67 -0
  261. package/src/core-skills/scm-shard-doc/SKILL.md +105 -0
  262. package/tools/format-workflow-md.js +263 -0
  263. package/tools/installer/README.md +60 -0
  264. package/tools/installer/cli-utils.js +181 -0
  265. package/tools/installer/commands/install.js +80 -0
  266. package/tools/installer/commands/status.js +65 -0
  267. package/tools/installer/commands/uninstall.js +167 -0
  268. package/tools/installer/core/config.js +52 -0
  269. package/tools/installer/core/custom-module-cache.js +260 -0
  270. package/tools/installer/core/existing-install.js +127 -0
  271. package/tools/installer/core/install-paths.js +129 -0
  272. package/tools/installer/core/installer.js +1790 -0
  273. package/tools/installer/core/manifest-generator.js +701 -0
  274. package/tools/installer/core/manifest.js +1040 -0
  275. package/tools/installer/custom-handler.js +112 -0
  276. package/tools/installer/external-official-modules.yaml +63 -0
  277. package/tools/installer/file-ops.js +204 -0
  278. package/tools/installer/ide/_config-driven.js +536 -0
  279. package/tools/installer/ide/manager.js +247 -0
  280. package/tools/installer/ide/platform-codes.js +37 -0
  281. package/tools/installer/ide/platform-codes.yaml +192 -0
  282. package/tools/installer/ide/shared/agent-command-generator.js +180 -0
  283. package/tools/installer/ide/shared/module-injections.js +136 -0
  284. package/tools/installer/ide/shared/path-utils.js +364 -0
  285. package/tools/installer/ide/shared/scm-artifacts.js +208 -0
  286. package/tools/installer/ide/shared/skill-manifest.js +72 -0
  287. package/tools/installer/ide/templates/agent-command-template.md +14 -0
  288. package/tools/installer/ide/templates/combined/antigravity.md +8 -0
  289. package/tools/installer/ide/templates/combined/default-agent.md +15 -0
  290. package/tools/installer/ide/templates/combined/default-task.md +10 -0
  291. package/tools/installer/ide/templates/combined/default-tool.md +10 -0
  292. package/tools/installer/ide/templates/combined/default-workflow.md +6 -0
  293. package/tools/installer/ide/templates/combined/gemini-agent.toml +14 -0
  294. package/tools/installer/ide/templates/combined/gemini-task.toml +11 -0
  295. package/tools/installer/ide/templates/combined/gemini-tool.toml +11 -0
  296. package/tools/installer/ide/templates/combined/gemini-workflow-yaml.toml +16 -0
  297. package/tools/installer/ide/templates/combined/gemini-workflow.toml +14 -0
  298. package/tools/installer/ide/templates/combined/kiro-agent.md +16 -0
  299. package/tools/installer/ide/templates/combined/kiro-task.md +9 -0
  300. package/tools/installer/ide/templates/combined/kiro-tool.md +9 -0
  301. package/tools/installer/ide/templates/combined/kiro-workflow.md +7 -0
  302. package/tools/installer/ide/templates/combined/opencode-agent.md +15 -0
  303. package/tools/installer/ide/templates/combined/opencode-task.md +13 -0
  304. package/tools/installer/ide/templates/combined/opencode-tool.md +13 -0
  305. package/tools/installer/ide/templates/combined/opencode-workflow-yaml.md +16 -0
  306. package/tools/installer/ide/templates/combined/opencode-workflow.md +16 -0
  307. package/tools/installer/ide/templates/combined/rovodev.md +9 -0
  308. package/tools/installer/ide/templates/combined/trae.md +9 -0
  309. package/tools/installer/ide/templates/combined/windsurf-workflow.md +10 -0
  310. package/tools/installer/ide/templates/split/.gitkeep +0 -0
  311. package/tools/installer/install-messages.yaml +35 -0
  312. package/tools/installer/message-loader.js +83 -0
  313. package/tools/installer/modules/custom-modules.js +197 -0
  314. package/tools/installer/modules/external-manager.js +354 -0
  315. package/tools/installer/modules/official-modules.js +2043 -0
  316. package/tools/installer/project-root.js +77 -0
  317. package/tools/installer/prompts.js +809 -0
  318. package/tools/installer/scm-cli.js +108 -0
  319. package/tools/installer/ui.js +1683 -0
  320. package/tools/installer/yaml-format.js +245 -0
  321. package/tools/javascript-conventions.md +5 -0
  322. package/tools/migrate-custom-module-paths.js +124 -0
  323. package/tools/platform-codes.yaml +169 -0
  324. package/tools/validate-skills.js +736 -0
@@ -0,0 +1,1683 @@
1
+ const path = require('node:path');
2
+ const os = require('node:os');
3
+ const fs = require('fs-extra');
4
+ const { CLIUtils } = require('./cli-utils');
5
+ const { CustomHandler } = require('./custom-handler');
6
+ const { ExternalModuleManager } = require('./modules/external-manager');
7
+ const prompts = require('./prompts');
8
+
9
+ // Separator class for visual grouping in select/multiselect prompts
10
+ // Note: @clack/prompts doesn't support separators natively, they are filtered out
11
+ class Separator {
12
+ constructor(text = '────────') {
13
+ this.line = text;
14
+ this.name = text;
15
+ }
16
+ type = 'separator';
17
+ }
18
+
19
+ // Separator for choice lists (compatible interface)
20
+ const choiceUtils = { Separator };
21
+
22
+ /**
23
+ * UI utilities for the installer
24
+ */
25
+ class UI {
26
+ /**
27
+ * Prompt for installation configuration
28
+ * @param {Object} options - Command-line options from install command
29
+ * @returns {Object} Installation configuration
30
+ */
31
+ async promptInstall(options = {}) {
32
+ await CLIUtils.displayLogo();
33
+
34
+ // Display version-specific start message from install-messages.yaml
35
+ const { MessageLoader } = require('./message-loader');
36
+ const messageLoader = new MessageLoader();
37
+ await messageLoader.displayStartMessage();
38
+
39
+ // Get directory from options or prompt
40
+ let confirmedDirectory;
41
+ if (options.directory) {
42
+ // Use provided directory from command-line
43
+ const expandedDir = this.expandUserPath(options.directory);
44
+ const validation = this.validateDirectorySync(expandedDir);
45
+ if (validation) {
46
+ throw new Error(`Invalid directory: ${validation}`);
47
+ }
48
+ confirmedDirectory = expandedDir;
49
+ await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`);
50
+ } else {
51
+ confirmedDirectory = await this.getConfirmedDirectory();
52
+ }
53
+
54
+ const { Installer } = require('./core/installer');
55
+ const installer = new Installer();
56
+ const { scmDir } = await installer.findBmadDir(confirmedDirectory);
57
+
58
+ // Check if there's an existing SCM installation
59
+ const hasExistingInstall = await fs.pathExists(scmDir);
60
+
61
+ let customContentConfig = { hasCustomContent: false };
62
+ if (!hasExistingInstall) {
63
+ customContentConfig._shouldAsk = true;
64
+ }
65
+
66
+ // Track action type (only set if there's an existing installation)
67
+ let actionType;
68
+
69
+ // Only show action menu if there's an existing installation
70
+ if (hasExistingInstall) {
71
+ // Get version information
72
+ const { existingInstall, scmDir } = await this.getExistingInstallation(confirmedDirectory);
73
+ const packageJsonPath = path.join(__dirname, '../../package.json');
74
+ const currentVersion = require(packageJsonPath).version;
75
+ const installedVersion = existingInstall.installed ? existingInstall.version || 'unknown' : 'unknown';
76
+
77
+ // Build menu choices dynamically
78
+ const choices = [];
79
+
80
+ // Always show Quick Update first (allows refreshing installation even on same version)
81
+ if (installedVersion !== 'unknown') {
82
+ choices.push({
83
+ name: `Quick Update (v${installedVersion} → v${currentVersion})`,
84
+ value: 'quick-update',
85
+ });
86
+ }
87
+
88
+ // Common actions
89
+ choices.push({ name: 'Modify SCM Installation', value: 'update' });
90
+
91
+ // Check if action is provided via command-line
92
+ if (options.action) {
93
+ const validActions = choices.map((c) => c.value);
94
+ if (!validActions.includes(options.action)) {
95
+ throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`);
96
+ }
97
+ actionType = options.action;
98
+ await prompts.log.info(`Using action from command-line: ${actionType}`);
99
+ } else if (options.yes) {
100
+ // Default to quick-update if available, otherwise first available choice
101
+ if (choices.length === 0) {
102
+ throw new Error('No valid actions available for this installation');
103
+ }
104
+ const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
105
+ actionType = hasQuickUpdate ? 'quick-update' : choices[0].value;
106
+ await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
107
+ } else {
108
+ actionType = await prompts.select({
109
+ message: 'How would you like to proceed?',
110
+ choices: choices,
111
+ default: choices[0].value,
112
+ });
113
+ }
114
+
115
+ // Handle quick update separately
116
+ if (actionType === 'quick-update') {
117
+ // Pass --custom-content through so installer can re-cache if cache is missing
118
+ let customContentForQuickUpdate = { hasCustomContent: false };
119
+ if (options.customContent) {
120
+ const paths = options.customContent
121
+ .split(',')
122
+ .map((p) => p.trim())
123
+ .filter(Boolean);
124
+ if (paths.length > 0) {
125
+ const customPaths = [];
126
+ const selectedModuleIds = [];
127
+ const sources = [];
128
+ for (const customPath of paths) {
129
+ const expandedPath = this.expandUserPath(customPath);
130
+ const validation = this.validateCustomContentPathSync(expandedPath);
131
+ if (validation) continue;
132
+ let moduleMeta;
133
+ try {
134
+ const moduleYamlPath = path.join(expandedPath, 'module.yaml');
135
+ moduleMeta = require('yaml').parse(await fs.readFile(moduleYamlPath, 'utf-8'));
136
+ } catch {
137
+ continue;
138
+ }
139
+ if (!moduleMeta?.code) continue;
140
+ customPaths.push(expandedPath);
141
+ selectedModuleIds.push(moduleMeta.code);
142
+ sources.push({ path: expandedPath, id: moduleMeta.code, name: moduleMeta.name || moduleMeta.code });
143
+ }
144
+ if (customPaths.length > 0) {
145
+ customContentForQuickUpdate = {
146
+ hasCustomContent: true,
147
+ selected: true,
148
+ sources,
149
+ selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
150
+ selectedModuleIds,
151
+ };
152
+ }
153
+ }
154
+ }
155
+ return {
156
+ actionType: 'quick-update',
157
+ directory: confirmedDirectory,
158
+ customContent: customContentForQuickUpdate,
159
+ skipPrompts: options.yes || false,
160
+ };
161
+ }
162
+
163
+ // If actionType === 'update', handle it with the new flow
164
+ // Return early with modify configuration
165
+ if (actionType === 'update') {
166
+ // Get existing installation info
167
+ const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
168
+
169
+ await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`);
170
+
171
+ // Unified module selection - all modules in one grouped multiselect
172
+ let selectedModules;
173
+ if (options.modules) {
174
+ // Use modules from command-line
175
+ selectedModules = options.modules
176
+ .split(',')
177
+ .map((m) => m.trim())
178
+ .filter(Boolean);
179
+ await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
180
+ } else if (options.yes) {
181
+ selectedModules = await this.getDefaultModules(installedModuleIds);
182
+ await prompts.log.info(
183
+ `Non-interactive mode (--yes): using default modules (installed + defaults): ${selectedModules.join(', ')}`,
184
+ );
185
+ } else {
186
+ selectedModules = await this.selectAllModules(installedModuleIds);
187
+ }
188
+
189
+ // After module selection, ask about custom modules
190
+ let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
191
+
192
+ if (options.customContent) {
193
+ // Use custom content from command-line
194
+ const paths = options.customContent
195
+ .split(',')
196
+ .map((p) => p.trim())
197
+ .filter(Boolean);
198
+ await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
199
+
200
+ // Build custom content config similar to promptCustomContentSource
201
+ const customPaths = [];
202
+ const selectedModuleIds = [];
203
+ const sources = [];
204
+
205
+ for (const customPath of paths) {
206
+ const expandedPath = this.expandUserPath(customPath);
207
+ const validation = this.validateCustomContentPathSync(expandedPath);
208
+ if (validation) {
209
+ await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
210
+ continue;
211
+ }
212
+
213
+ // Read module metadata
214
+ let moduleMeta;
215
+ try {
216
+ const moduleYamlPath = path.join(expandedPath, 'module.yaml');
217
+ const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
218
+ const yaml = require('yaml');
219
+ moduleMeta = yaml.parse(moduleYaml);
220
+ } catch (error) {
221
+ await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
222
+ continue;
223
+ }
224
+
225
+ if (!moduleMeta) {
226
+ await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
227
+ continue;
228
+ }
229
+
230
+ if (!moduleMeta.code) {
231
+ await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
232
+ continue;
233
+ }
234
+
235
+ customPaths.push(expandedPath);
236
+ selectedModuleIds.push(moduleMeta.code);
237
+ sources.push({
238
+ path: expandedPath,
239
+ id: moduleMeta.code,
240
+ name: moduleMeta.name || moduleMeta.code,
241
+ });
242
+ }
243
+
244
+ if (customPaths.length > 0) {
245
+ customModuleResult = {
246
+ selectedCustomModules: selectedModuleIds,
247
+ customContentConfig: {
248
+ hasCustomContent: true,
249
+ selected: true,
250
+ sources,
251
+ selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
252
+ selectedModuleIds: selectedModuleIds,
253
+ },
254
+ };
255
+ }
256
+ } else if (options.yes) {
257
+ // Non-interactive mode: preserve existing custom modules (matches default: false)
258
+ const cacheDir = path.join(scmDir, '_config', 'custom');
259
+ if (await fs.pathExists(cacheDir)) {
260
+ const entries = await fs.readdir(cacheDir, { withFileTypes: true });
261
+ for (const entry of entries) {
262
+ if (entry.isDirectory()) {
263
+ customModuleResult.selectedCustomModules.push(entry.name);
264
+ }
265
+ }
266
+ await prompts.log.info(
267
+ `Non-interactive mode (--yes): preserving ${customModuleResult.selectedCustomModules.length} existing custom module(s)`,
268
+ );
269
+ } else {
270
+ await prompts.log.info('Non-interactive mode (--yes): no existing custom modules found');
271
+ }
272
+ } else {
273
+ const changeCustomModules = await prompts.confirm({
274
+ message: 'Modify custom modules, agents, or workflows?',
275
+ default: false,
276
+ });
277
+
278
+ if (changeCustomModules) {
279
+ customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
280
+ } else {
281
+ // Preserve existing custom modules if user doesn't want to modify them
282
+ const { Installer } = require('./core/installer');
283
+ const installer = new Installer();
284
+ const { scmDir } = await installer.findBmadDir(confirmedDirectory);
285
+
286
+ const cacheDir = path.join(scmDir, '_config', 'custom');
287
+ if (await fs.pathExists(cacheDir)) {
288
+ const entries = await fs.readdir(cacheDir, { withFileTypes: true });
289
+ for (const entry of entries) {
290
+ if (entry.isDirectory()) {
291
+ customModuleResult.selectedCustomModules.push(entry.name);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ // Merge any selected custom modules
299
+ if (customModuleResult.selectedCustomModules.length > 0) {
300
+ selectedModules.push(...customModuleResult.selectedCustomModules);
301
+ }
302
+
303
+ // Ensure core is in the modules list
304
+ if (!selectedModules.includes('core')) {
305
+ selectedModules.unshift('core');
306
+ }
307
+
308
+ // Get tool selection
309
+ const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
310
+
311
+ const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, options);
312
+
313
+ return {
314
+ actionType: 'update',
315
+ directory: confirmedDirectory,
316
+ modules: selectedModules,
317
+ ides: toolSelection.ides,
318
+ skipIde: toolSelection.skipIde,
319
+ coreConfig: moduleConfigs.core || {},
320
+ moduleConfigs: moduleConfigs,
321
+ customContent: customModuleResult.customContentConfig,
322
+ skipPrompts: options.yes || false,
323
+ };
324
+ }
325
+ }
326
+
327
+ // This section is only for new installations (update returns early above)
328
+ const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
329
+
330
+ // Unified module selection - all modules in one grouped multiselect
331
+ let selectedModules;
332
+ if (options.modules) {
333
+ // Use modules from command-line
334
+ selectedModules = options.modules
335
+ .split(',')
336
+ .map((m) => m.trim())
337
+ .filter(Boolean);
338
+ await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
339
+ } else if (options.yes) {
340
+ // Use default modules when --yes flag is set
341
+ selectedModules = await this.getDefaultModules(installedModuleIds);
342
+ await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`);
343
+ } else {
344
+ selectedModules = await this.selectAllModules(installedModuleIds);
345
+ }
346
+
347
+ // Ask about custom content (local modules/agents/workflows)
348
+ if (options.customContent) {
349
+ // Use custom content from command-line
350
+ const paths = options.customContent
351
+ .split(',')
352
+ .map((p) => p.trim())
353
+ .filter(Boolean);
354
+ await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
355
+
356
+ // Build custom content config similar to promptCustomContentSource
357
+ const customPaths = [];
358
+ const selectedModuleIds = [];
359
+ const sources = [];
360
+
361
+ for (const customPath of paths) {
362
+ const expandedPath = this.expandUserPath(customPath);
363
+ const validation = this.validateCustomContentPathSync(expandedPath);
364
+ if (validation) {
365
+ await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
366
+ continue;
367
+ }
368
+
369
+ // Read module metadata
370
+ let moduleMeta;
371
+ try {
372
+ const moduleYamlPath = path.join(expandedPath, 'module.yaml');
373
+ const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
374
+ const yaml = require('yaml');
375
+ moduleMeta = yaml.parse(moduleYaml);
376
+ } catch (error) {
377
+ await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
378
+ continue;
379
+ }
380
+
381
+ if (!moduleMeta) {
382
+ await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
383
+ continue;
384
+ }
385
+
386
+ if (!moduleMeta.code) {
387
+ await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
388
+ continue;
389
+ }
390
+
391
+ customPaths.push(expandedPath);
392
+ selectedModuleIds.push(moduleMeta.code);
393
+ sources.push({
394
+ path: expandedPath,
395
+ id: moduleMeta.code,
396
+ name: moduleMeta.name || moduleMeta.code,
397
+ });
398
+ }
399
+
400
+ if (customPaths.length > 0) {
401
+ customContentConfig = {
402
+ hasCustomContent: true,
403
+ selected: true,
404
+ sources,
405
+ selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
406
+ selectedModuleIds: selectedModuleIds,
407
+ };
408
+ }
409
+ } else if (!options.yes) {
410
+ const wantsCustomContent = await prompts.confirm({
411
+ message: 'Add custom modules, agents, or workflows from your computer?',
412
+ default: false,
413
+ });
414
+
415
+ if (wantsCustomContent) {
416
+ customContentConfig = await this.promptCustomContentSource();
417
+ }
418
+ }
419
+
420
+ // Add custom content modules if any were selected
421
+ if (customContentConfig && customContentConfig.selectedModuleIds) {
422
+ selectedModules.push(...customContentConfig.selectedModuleIds);
423
+ }
424
+
425
+ // Ensure core is in the modules list
426
+ if (!selectedModules.includes('core')) {
427
+ selectedModules.unshift('core');
428
+ }
429
+ let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
430
+ const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, options);
431
+
432
+ return {
433
+ actionType: 'install',
434
+ directory: confirmedDirectory,
435
+ modules: selectedModules,
436
+ ides: toolSelection.ides,
437
+ skipIde: toolSelection.skipIde,
438
+ coreConfig: moduleConfigs.core || {},
439
+ moduleConfigs: moduleConfigs,
440
+ customContent: customContentConfig,
441
+ skipPrompts: options.yes || false,
442
+ };
443
+ }
444
+
445
+ /**
446
+ * Prompt for tool/IDE selection (called after module configuration)
447
+ * Uses a split prompt approach:
448
+ * 1. Recommended tools - standard multiselect for preferred tools
449
+ * 2. Additional tools - autocompleteMultiselect with search capability
450
+ * @param {string} projectDir - Project directory to check for existing IDEs
451
+ * @param {Object} options - Command-line options
452
+ * @returns {Object} Tool configuration
453
+ */
454
+ async promptToolSelection(projectDir, options = {}) {
455
+ const { ExistingInstall } = require('./core/existing-install');
456
+ const { Installer } = require('./core/installer');
457
+ const installer = new Installer();
458
+ const { scmDir } = await installer.findBmadDir(projectDir || process.cwd());
459
+ const existingInstall = await ExistingInstall.detect(scmDir);
460
+ const configuredIdes = existingInstall.ides;
461
+
462
+ // Get IDE manager to fetch available IDEs dynamically
463
+ const { IdeManager } = require('./ide/manager');
464
+ const ideManager = new IdeManager();
465
+ await ideManager.ensureInitialized(); // IMPORTANT: Must initialize before getting IDEs
466
+
467
+ const preferredIdes = ideManager.getPreferredIdes();
468
+ const otherIdes = ideManager.getOtherIdes();
469
+
470
+ // Determine which configured IDEs are in "preferred" vs "other" categories
471
+ const configuredPreferred = configuredIdes.filter((id) => preferredIdes.some((ide) => ide.value === id));
472
+ const configuredOther = configuredIdes.filter((id) => otherIdes.some((ide) => ide.value === id));
473
+
474
+ // Warn about previously configured tools that are no longer available
475
+ const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value));
476
+ const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id));
477
+ if (unknownTools.length > 0) {
478
+ await prompts.log.warn(`Previously configured tools are no longer available: ${unknownTools.join(', ')}`);
479
+ }
480
+
481
+ // ─────────────────────────────────────────────────────────────────────────────
482
+ // UPGRADE PATH: If tools already configured, show all tools with configured at top
483
+ // ─────────────────────────────────────────────────────────────────────────────
484
+ if (configuredIdes.length > 0) {
485
+ const allTools = [...preferredIdes, ...otherIdes];
486
+
487
+ // Non-interactive: handle --tools and --yes flags before interactive prompt
488
+ if (options.tools) {
489
+ if (options.tools.toLowerCase() === 'none') {
490
+ await prompts.log.info('Skipping tool configuration (--tools none)');
491
+ return { ides: [], skipIde: true };
492
+ }
493
+ const selectedIdes = options.tools
494
+ .split(',')
495
+ .map((t) => t.trim())
496
+ .filter(Boolean);
497
+ await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
498
+ await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
499
+ return { ides: selectedIdes, skipIde: false };
500
+ }
501
+
502
+ if (options.yes) {
503
+ await prompts.log.info(`Non-interactive mode (--yes): keeping configured tools: ${configuredIdes.join(', ')}`);
504
+ await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
505
+ return { ides: configuredIdes, skipIde: false };
506
+ }
507
+
508
+ // Sort: configured tools first, then preferred, then others
509
+ const sortedTools = [
510
+ ...allTools.filter((ide) => configuredIdes.includes(ide.value)),
511
+ ...allTools.filter((ide) => !configuredIdes.includes(ide.value)),
512
+ ];
513
+
514
+ const upgradeOptions = sortedTools.map((ide) => {
515
+ const isConfigured = configuredIdes.includes(ide.value);
516
+ const isPreferred = preferredIdes.some((p) => p.value === ide.value);
517
+ let label = ide.name;
518
+ if (isPreferred) label += ' ⭐';
519
+ if (isConfigured) label += ' ✅';
520
+ return { label, value: ide.value };
521
+ });
522
+
523
+ // Sort initialValues to match display order
524
+ const sortedInitialValues = sortedTools.filter((ide) => configuredIdes.includes(ide.value)).map((ide) => ide.value);
525
+
526
+ const upgradeSelected = await prompts.autocompleteMultiselect({
527
+ message: 'Integrate with',
528
+ options: upgradeOptions,
529
+ initialValues: sortedInitialValues,
530
+ required: false,
531
+ maxItems: 8,
532
+ });
533
+
534
+ const selectedIdes = upgradeSelected || [];
535
+
536
+ if (selectedIdes.length === 0) {
537
+ const confirmNoTools = await prompts.confirm({
538
+ message: 'No tools selected. Continue without installing any tools?',
539
+ default: false,
540
+ });
541
+
542
+ if (!confirmNoTools) {
543
+ return this.promptToolSelection(projectDir, options);
544
+ }
545
+
546
+ return { ides: [], skipIde: true };
547
+ }
548
+
549
+ // Display selected tools
550
+ await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
551
+
552
+ return { ides: selectedIdes, skipIde: false };
553
+ }
554
+
555
+ // ─────────────────────────────────────────────────────────────────────────────
556
+ // NEW INSTALL: Show all tools with search
557
+ // ─────────────────────────────────────────────────────────────────────────────
558
+ const allTools = [...preferredIdes, ...otherIdes];
559
+
560
+ const allToolOptions = allTools.map((ide) => {
561
+ const isPreferred = preferredIdes.some((p) => p.value === ide.value);
562
+ let label = ide.name;
563
+ if (isPreferred) label += ' ⭐';
564
+ return {
565
+ label,
566
+ value: ide.value,
567
+ };
568
+ });
569
+
570
+ let selectedIdes = [];
571
+
572
+ // Check if tools are provided via command-line
573
+ if (options.tools) {
574
+ // Check for explicit "none" value to skip tool installation
575
+ if (options.tools.toLowerCase() === 'none') {
576
+ await prompts.log.info('Skipping tool configuration (--tools none)');
577
+ return { ides: [], skipIde: true };
578
+ } else {
579
+ selectedIdes = options.tools
580
+ .split(',')
581
+ .map((t) => t.trim())
582
+ .filter(Boolean);
583
+ await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
584
+ await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
585
+ return { ides: selectedIdes, skipIde: false };
586
+ }
587
+ } else if (options.yes) {
588
+ // If --yes flag is set, skip tool prompt and use previously configured tools or empty
589
+ if (configuredIdes.length > 0) {
590
+ await prompts.log.info(`Using previously configured tools (--yes flag): ${configuredIdes.join(', ')}`);
591
+ await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
592
+ return { ides: configuredIdes, skipIde: false };
593
+ } else {
594
+ await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)');
595
+ return { ides: [], skipIde: true };
596
+ }
597
+ }
598
+
599
+ // Interactive mode
600
+ const interactiveSelectedIdes = await prompts.autocompleteMultiselect({
601
+ message: 'Integrate with:',
602
+ options: allToolOptions,
603
+ initialValues: configuredIdes.length > 0 ? configuredIdes : undefined,
604
+ required: false,
605
+ maxItems: 8,
606
+ });
607
+
608
+ selectedIdes = interactiveSelectedIdes || [];
609
+
610
+ // ─────────────────────────────────────────────────────────────────────────────
611
+ // STEP 3: Confirm if no tools selected
612
+ // ─────────────────────────────────────────────────────────────────────────────
613
+ if (selectedIdes.length === 0) {
614
+ const confirmNoTools = await prompts.confirm({
615
+ message: 'No tools selected. Continue without installing any tools?',
616
+ default: false,
617
+ });
618
+
619
+ if (!confirmNoTools) {
620
+ // User wants to select tools - recurse
621
+ return this.promptToolSelection(projectDir, options);
622
+ }
623
+
624
+ return {
625
+ ides: [],
626
+ skipIde: true,
627
+ };
628
+ }
629
+
630
+ // Display selected tools
631
+ await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
632
+
633
+ return {
634
+ ides: selectedIdes,
635
+ skipIde: selectedIdes.length === 0,
636
+ };
637
+ }
638
+
639
+ /**
640
+ * Prompt for update configuration
641
+ * @returns {Object} Update configuration
642
+ */
643
+ async promptUpdate() {
644
+ const backupFirst = await prompts.confirm({
645
+ message: 'Create backup before updating?',
646
+ default: true,
647
+ });
648
+
649
+ const preserveCustomizations = await prompts.confirm({
650
+ message: 'Preserve local customizations?',
651
+ default: true,
652
+ });
653
+
654
+ return { backupFirst, preserveCustomizations };
655
+ }
656
+
657
+ /**
658
+ * Confirm action
659
+ * @param {string} message - Confirmation message
660
+ * @param {boolean} defaultValue - Default value
661
+ * @returns {boolean} User confirmation
662
+ */
663
+ async confirm(message, defaultValue = false) {
664
+ return await prompts.confirm({
665
+ message,
666
+ default: defaultValue,
667
+ });
668
+ }
669
+
670
+ /**
671
+ * Get confirmed directory from user
672
+ * @returns {string} Confirmed directory path
673
+ */
674
+ async getConfirmedDirectory() {
675
+ let confirmedDirectory = null;
676
+ while (!confirmedDirectory) {
677
+ const directoryAnswer = await this.promptForDirectory();
678
+ await this.displayDirectoryInfo(directoryAnswer.directory);
679
+
680
+ if (await this.confirmDirectory(directoryAnswer.directory)) {
681
+ confirmedDirectory = directoryAnswer.directory;
682
+ }
683
+ }
684
+ return confirmedDirectory;
685
+ }
686
+
687
+ /**
688
+ * Get existing installation info and installed modules
689
+ * @param {string} directory - Installation directory
690
+ * @returns {Object} Object with existingInstall, installedModuleIds, and scmDir
691
+ */
692
+ async getExistingInstallation(directory) {
693
+ const { ExistingInstall } = require('./core/existing-install');
694
+ const { Installer } = require('./core/installer');
695
+ const installer = new Installer();
696
+ const { scmDir } = await installer.findBmadDir(directory);
697
+ const existingInstall = await ExistingInstall.detect(scmDir);
698
+ const installedModuleIds = new Set(existingInstall.moduleIds);
699
+
700
+ return { existingInstall, installedModuleIds, scmDir };
701
+ }
702
+
703
+ /**
704
+ * Collect all module configurations (core + selected modules).
705
+ * All interactive prompting happens here in the UI layer.
706
+ * @param {string} directory - Installation directory
707
+ * @param {string[]} modules - Modules to configure (including 'core')
708
+ * @param {Object} options - Command-line options
709
+ * @returns {Object} Collected module configurations keyed by module name
710
+ */
711
+ async collectModuleConfigs(directory, modules, options = {}) {
712
+ const { OfficialModules } = require('./modules/official-modules');
713
+ const configCollector = new OfficialModules();
714
+
715
+ // Seed core config from CLI options if provided
716
+ if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) {
717
+ const coreConfig = {};
718
+ if (options.userName) {
719
+ coreConfig.user_name = options.userName;
720
+ await prompts.log.info(`Using user name from command-line: ${options.userName}`);
721
+ }
722
+ if (options.communicationLanguage) {
723
+ coreConfig.communication_language = options.communicationLanguage;
724
+ await prompts.log.info(`Using communication language from command-line: ${options.communicationLanguage}`);
725
+ }
726
+ if (options.documentOutputLanguage) {
727
+ coreConfig.document_output_language = options.documentOutputLanguage;
728
+ await prompts.log.info(`Using document output language from command-line: ${options.documentOutputLanguage}`);
729
+ }
730
+ if (options.outputFolder) {
731
+ coreConfig.output_folder = options.outputFolder;
732
+ await prompts.log.info(`Using output folder from command-line: ${options.outputFolder}`);
733
+ }
734
+
735
+ // Load existing config to merge with provided options
736
+ await configCollector.loadExistingConfig(directory);
737
+ const existingConfig = configCollector.collectedConfig.core || {};
738
+ configCollector.collectedConfig.core = { ...existingConfig, ...coreConfig };
739
+
740
+ // If not all options are provided, collect the missing ones interactively (unless --yes flag)
741
+ if (
742
+ !options.yes &&
743
+ (!options.userName || !options.communicationLanguage || !options.documentOutputLanguage || !options.outputFolder)
744
+ ) {
745
+ await configCollector.collectModuleConfig('core', directory, false, true);
746
+ }
747
+ } else if (options.yes) {
748
+ // Use all defaults when --yes flag is set
749
+ await configCollector.loadExistingConfig(directory);
750
+ const existingConfig = configCollector.collectedConfig.core || {};
751
+
752
+ if (Object.keys(existingConfig).length === 0) {
753
+ let safeUsername;
754
+ try {
755
+ safeUsername = os.userInfo().username;
756
+ } catch {
757
+ safeUsername = process.env.USER || process.env.USERNAME || 'User';
758
+ }
759
+ const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1);
760
+ configCollector.collectedConfig.core = {
761
+ user_name: defaultUsername,
762
+ communication_language: 'English',
763
+ document_output_language: 'English',
764
+ output_folder: '_scm-output',
765
+ };
766
+ await prompts.log.info('Using default configuration (--yes flag)');
767
+ }
768
+ }
769
+
770
+ // Collect all module configs — core is skipped if already seeded above
771
+ await configCollector.collectAllConfigurations(modules, directory, {
772
+ skipPrompts: options.yes || false,
773
+ });
774
+
775
+ return configCollector.collectedConfig;
776
+ }
777
+
778
+ /**
779
+ * Get module choices for selection
780
+ * @param {Set} installedModuleIds - Currently installed module IDs
781
+ * @param {Object} customContentConfig - Custom content configuration
782
+ * @returns {Array} Module choices for prompt
783
+ */
784
+ async getModuleChoices(installedModuleIds, customContentConfig = null) {
785
+ const color = await prompts.getColor();
786
+ const moduleChoices = [];
787
+ const isNewInstallation = installedModuleIds.size === 0;
788
+
789
+ const customContentItems = [];
790
+
791
+ // Add custom content items
792
+ if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
793
+ // Existing installation - show from directory
794
+ const customHandler = new CustomHandler();
795
+ const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
796
+
797
+ for (const customFile of customFiles) {
798
+ const customInfo = await customHandler.getCustomInfo(customFile);
799
+ if (customInfo) {
800
+ customContentItems.push({
801
+ name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`,
802
+ value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
803
+ checked: true, // Default to selected since user chose to provide custom content
804
+ path: customInfo.path, // Track path to avoid duplicates
805
+ hint: customInfo.description || undefined,
806
+ });
807
+ }
808
+ }
809
+ }
810
+
811
+ // Add official modules
812
+ const { OfficialModules } = require('./modules/official-modules');
813
+ const officialModules = new OfficialModules();
814
+ const { modules: availableModules, customModules: customModulesFromCache } = await officialModules.listAvailable();
815
+
816
+ // First, add all items to appropriate sections
817
+ const allCustomModules = [];
818
+
819
+ // Add custom content items from directory
820
+ allCustomModules.push(...customContentItems);
821
+
822
+ // Add custom modules from cache
823
+ for (const mod of customModulesFromCache) {
824
+ // Skip if this module is already in customContentItems (by path)
825
+ const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path));
826
+
827
+ if (!isDuplicate) {
828
+ allCustomModules.push({
829
+ name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`,
830
+ value: mod.id,
831
+ checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
832
+ hint: mod.description || undefined,
833
+ });
834
+ }
835
+ }
836
+
837
+ // Add separators and modules in correct order
838
+ if (allCustomModules.length > 0) {
839
+ // Add separator for custom content, all custom modules, and official content separator
840
+ moduleChoices.push(
841
+ new choiceUtils.Separator('── Custom Content ──'),
842
+ ...allCustomModules,
843
+ new choiceUtils.Separator('── Official Content ──'),
844
+ );
845
+ }
846
+
847
+ // Add official modules (only non-custom ones)
848
+ for (const mod of availableModules) {
849
+ if (!mod.isCustom) {
850
+ moduleChoices.push({
851
+ name: mod.name,
852
+ value: mod.id,
853
+ checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
854
+ hint: mod.description || undefined,
855
+ });
856
+ }
857
+ }
858
+
859
+ return moduleChoices;
860
+ }
861
+
862
+ /**
863
+ * Select all modules (official + community) using grouped multiselect.
864
+ * Core is shown as locked but filtered from the result since it's always installed separately.
865
+ * @param {Set} installedModuleIds - Currently installed module IDs
866
+ * @returns {Array} Selected module codes (excluding core)
867
+ */
868
+ async selectAllModules(installedModuleIds = new Set()) {
869
+ const { OfficialModules } = require('./modules/official-modules');
870
+ const officialModulesSource = new OfficialModules();
871
+ const { modules: localModules } = await officialModulesSource.listAvailable();
872
+
873
+ // Get external modules
874
+ const externalManager = new ExternalModuleManager();
875
+ const externalModules = await externalManager.listAvailable();
876
+
877
+ // Build flat options list with group hints for autocompleteMultiselect
878
+ const allOptions = [];
879
+ const initialValues = [];
880
+ const lockedValues = ['core'];
881
+
882
+ // Core module is always installed — show it locked at the top
883
+ allOptions.push({ label: 'SCM Core Module', value: 'core', hint: 'Core configuration and shared resources' });
884
+ initialValues.push('core');
885
+
886
+ // Helper to build module entry with proper sorting and selection
887
+ const buildModuleEntry = (mod, value, group) => {
888
+ const isInstalled = installedModuleIds.has(value);
889
+ return {
890
+ label: mod.name,
891
+ value,
892
+ hint: mod.description || group,
893
+ // Pre-select only if already installed (not on fresh install)
894
+ selected: isInstalled,
895
+ };
896
+ };
897
+
898
+ // Local modules (BMM, BMB, etc.)
899
+ const localEntries = [];
900
+ for (const mod of localModules) {
901
+ if (!mod.isCustom && mod.id !== 'core') {
902
+ const entry = buildModuleEntry(mod, mod.id, 'Local');
903
+ localEntries.push(entry);
904
+ if (entry.selected) {
905
+ initialValues.push(mod.id);
906
+ }
907
+ }
908
+ }
909
+ allOptions.push(...localEntries.map(({ label, value, hint }) => ({ label, value, hint })));
910
+
911
+ // Group 2: SCM Official Modules (type: scm-org)
912
+ const officialModules = [];
913
+ for (const mod of externalModules) {
914
+ if (mod.type === 'scm-org') {
915
+ const entry = buildModuleEntry(mod, mod.code, 'Official');
916
+ officialModules.push(entry);
917
+ if (entry.selected) {
918
+ initialValues.push(mod.code);
919
+ }
920
+ }
921
+ }
922
+ allOptions.push(...officialModules.map(({ label, value, hint }) => ({ label, value, hint })));
923
+
924
+ // Group 3: Community Modules (type: community)
925
+ const communityModules = [];
926
+ for (const mod of externalModules) {
927
+ if (mod.type === 'community') {
928
+ const entry = buildModuleEntry(mod, mod.code, 'Community');
929
+ communityModules.push(entry);
930
+ if (entry.selected) {
931
+ initialValues.push(mod.code);
932
+ }
933
+ }
934
+ }
935
+ allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));
936
+
937
+ const selected = await prompts.autocompleteMultiselect({
938
+ message: 'Select modules to install:',
939
+ options: allOptions,
940
+ initialValues: initialValues.length > 0 ? initialValues : undefined,
941
+ lockedValues,
942
+ required: true,
943
+ maxItems: allOptions.length,
944
+ });
945
+
946
+ const result = selected ? [...selected] : [];
947
+
948
+ // Display selected modules as bulleted list
949
+ if (result.length > 0) {
950
+ const moduleLines = result.map((moduleId) => {
951
+ const opt = allOptions.find((o) => o.value === moduleId);
952
+ return ` \u2022 ${opt?.label || moduleId}`;
953
+ });
954
+ await prompts.log.message('Selected modules:\n' + moduleLines.join('\n'));
955
+ }
956
+
957
+ return result;
958
+ }
959
+
960
+ /**
961
+ * Get default modules for non-interactive mode
962
+ * @param {Set} installedModuleIds - Already installed module IDs
963
+ * @returns {Array} Default module codes
964
+ */
965
+ async getDefaultModules(installedModuleIds = new Set()) {
966
+ const { OfficialModules } = require('./modules/official-modules');
967
+ const officialModules = new OfficialModules();
968
+ const { modules: localModules } = await officialModules.listAvailable();
969
+
970
+ const defaultModules = [];
971
+
972
+ // Add default-selected local modules (typically BMM)
973
+ for (const mod of localModules) {
974
+ if (mod.defaultSelected === true || installedModuleIds.has(mod.id)) {
975
+ defaultModules.push(mod.id);
976
+ }
977
+ }
978
+
979
+ // If no defaults found, use 'bmm' as the fallback default
980
+ if (defaultModules.length === 0) {
981
+ defaultModules.push('bmm');
982
+ }
983
+
984
+ return defaultModules;
985
+ }
986
+
987
+ /**
988
+ * Prompt for directory selection
989
+ * @returns {Object} Directory answer from prompt
990
+ */
991
+ async promptForDirectory() {
992
+ // Use sync validation because @clack/prompts doesn't support async validate
993
+ const directory = await prompts.text({
994
+ message: 'Installation directory:',
995
+ default: process.cwd(),
996
+ placeholder: process.cwd(),
997
+ validate: (input) => this.validateDirectorySync(input),
998
+ });
999
+
1000
+ // Apply filter logic
1001
+ let filteredDir = directory;
1002
+ if (!filteredDir || filteredDir.trim() === '') {
1003
+ filteredDir = process.cwd();
1004
+ } else {
1005
+ filteredDir = this.expandUserPath(filteredDir);
1006
+ }
1007
+
1008
+ return { directory: filteredDir };
1009
+ }
1010
+
1011
+ /**
1012
+ * Display directory information
1013
+ * @param {string} directory - The directory path
1014
+ */
1015
+ async displayDirectoryInfo(directory) {
1016
+ await prompts.log.info(`Resolved installation path: ${directory}`);
1017
+
1018
+ const dirExists = await fs.pathExists(directory);
1019
+ if (dirExists) {
1020
+ // Show helpful context about the existing path
1021
+ const stats = await fs.stat(directory);
1022
+ if (stats.isDirectory()) {
1023
+ const files = await fs.readdir(directory);
1024
+ if (files.length > 0) {
1025
+ // Check for any scm installation (any folder with _config/manifest.yaml)
1026
+ const { Installer } = require('./core/installer');
1027
+ const installer = new Installer();
1028
+ const scmResult = await installer.findBmadDir(directory);
1029
+ const hasBmadInstall =
1030
+ (await fs.pathExists(scmResult.scmDir)) && (await fs.pathExists(path.join(scmResult.scmDir, '_config', 'manifest.yaml')));
1031
+
1032
+ const scmNote = hasBmadInstall ? ` including existing SCM installation (${path.basename(scmResult.scmDir)})` : '';
1033
+ await prompts.log.message(`Directory exists and contains ${files.length} item(s)${scmNote}`);
1034
+ } else {
1035
+ await prompts.log.message('Directory exists and is empty');
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ /**
1042
+ * Confirm directory selection
1043
+ * @param {string} directory - The directory path
1044
+ * @returns {boolean} Whether user confirmed
1045
+ */
1046
+ async confirmDirectory(directory) {
1047
+ const dirExists = await fs.pathExists(directory);
1048
+
1049
+ if (dirExists) {
1050
+ const proceed = await prompts.confirm({
1051
+ message: 'Install to this directory?',
1052
+ default: true,
1053
+ });
1054
+
1055
+ if (!proceed) {
1056
+ await prompts.log.warn("Let's try again with a different path.");
1057
+ }
1058
+
1059
+ return proceed;
1060
+ } else {
1061
+ // Ask for confirmation to create the directory
1062
+ const create = await prompts.confirm({
1063
+ message: `Create directory: ${directory}?`,
1064
+ default: false,
1065
+ });
1066
+
1067
+ if (!create) {
1068
+ await prompts.log.warn("Let's try again with a different path.");
1069
+ }
1070
+
1071
+ return create;
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * Validate directory path for installation (sync version for clack prompts)
1077
+ * @param {string} input - User input path
1078
+ * @returns {string|undefined} Error message or undefined if valid
1079
+ */
1080
+ validateDirectorySync(input) {
1081
+ // Allow empty input to use the default
1082
+ if (!input || input.trim() === '') {
1083
+ return; // Empty means use default, undefined = valid for clack
1084
+ }
1085
+
1086
+ let expandedPath;
1087
+ try {
1088
+ expandedPath = this.expandUserPath(input.trim());
1089
+ } catch (error) {
1090
+ return error.message;
1091
+ }
1092
+
1093
+ // Check if the path exists
1094
+ const pathExists = fs.pathExistsSync(expandedPath);
1095
+
1096
+ if (!pathExists) {
1097
+ // Find the first existing parent directory
1098
+ const existingParent = this.findExistingParentSync(expandedPath);
1099
+
1100
+ if (!existingParent) {
1101
+ return 'Cannot create directory: no existing parent directory found';
1102
+ }
1103
+
1104
+ // Check if the existing parent is writable
1105
+ try {
1106
+ fs.accessSync(existingParent, fs.constants.W_OK);
1107
+ // Path doesn't exist but can be created - will prompt for confirmation later
1108
+ return;
1109
+ } catch {
1110
+ // Provide a detailed error message explaining both issues
1111
+ return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
1112
+ }
1113
+ }
1114
+
1115
+ // If it exists, validate it's a directory and writable
1116
+ const stat = fs.statSync(expandedPath);
1117
+ if (!stat.isDirectory()) {
1118
+ return `Path exists but is not a directory: ${expandedPath}`;
1119
+ }
1120
+
1121
+ // Check write permissions
1122
+ try {
1123
+ fs.accessSync(expandedPath, fs.constants.W_OK);
1124
+ } catch {
1125
+ return `Directory is not writable: ${expandedPath}`;
1126
+ }
1127
+
1128
+ return;
1129
+ }
1130
+
1131
+ /**
1132
+ * Validate directory path for installation (async version)
1133
+ * @param {string} input - User input path
1134
+ * @returns {string|true} Error message or true if valid
1135
+ */
1136
+ async validateDirectory(input) {
1137
+ // Allow empty input to use the default
1138
+ if (!input || input.trim() === '') {
1139
+ return true; // Empty means use default
1140
+ }
1141
+
1142
+ let expandedPath;
1143
+ try {
1144
+ expandedPath = this.expandUserPath(input.trim());
1145
+ } catch (error) {
1146
+ return error.message;
1147
+ }
1148
+
1149
+ // Check if the path exists
1150
+ const pathExists = await fs.pathExists(expandedPath);
1151
+
1152
+ if (!pathExists) {
1153
+ // Find the first existing parent directory
1154
+ const existingParent = await this.findExistingParent(expandedPath);
1155
+
1156
+ if (!existingParent) {
1157
+ return 'Cannot create directory: no existing parent directory found';
1158
+ }
1159
+
1160
+ // Check if the existing parent is writable
1161
+ try {
1162
+ await fs.access(existingParent, fs.constants.W_OK);
1163
+ // Path doesn't exist but can be created - will prompt for confirmation later
1164
+ return true;
1165
+ } catch {
1166
+ // Provide a detailed error message explaining both issues
1167
+ return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
1168
+ }
1169
+ }
1170
+
1171
+ // If it exists, validate it's a directory and writable
1172
+ const stat = await fs.stat(expandedPath);
1173
+ if (!stat.isDirectory()) {
1174
+ return `Path exists but is not a directory: ${expandedPath}`;
1175
+ }
1176
+
1177
+ // Check write permissions
1178
+ try {
1179
+ await fs.access(expandedPath, fs.constants.W_OK);
1180
+ } catch {
1181
+ return `Directory is not writable: ${expandedPath}`;
1182
+ }
1183
+
1184
+ return true;
1185
+ }
1186
+
1187
+ /**
1188
+ * Find the first existing parent directory (sync version)
1189
+ * @param {string} targetPath - The path to check
1190
+ * @returns {string|null} The first existing parent directory, or null if none found
1191
+ */
1192
+ findExistingParentSync(targetPath) {
1193
+ let currentPath = path.resolve(targetPath);
1194
+
1195
+ // Walk up the directory tree until we find an existing directory
1196
+ while (currentPath !== path.dirname(currentPath)) {
1197
+ // Stop at root
1198
+ const parent = path.dirname(currentPath);
1199
+ if (fs.pathExistsSync(parent)) {
1200
+ return parent;
1201
+ }
1202
+ currentPath = parent;
1203
+ }
1204
+
1205
+ return null; // No existing parent found (shouldn't happen in practice)
1206
+ }
1207
+
1208
+ /**
1209
+ * Find the first existing parent directory (async version)
1210
+ * @param {string} targetPath - The path to check
1211
+ * @returns {string|null} The first existing parent directory, or null if none found
1212
+ */
1213
+ async findExistingParent(targetPath) {
1214
+ let currentPath = path.resolve(targetPath);
1215
+
1216
+ // Walk up the directory tree until we find an existing directory
1217
+ while (currentPath !== path.dirname(currentPath)) {
1218
+ // Stop at root
1219
+ const parent = path.dirname(currentPath);
1220
+ if (await fs.pathExists(parent)) {
1221
+ return parent;
1222
+ }
1223
+ currentPath = parent;
1224
+ }
1225
+
1226
+ return null; // No existing parent found (shouldn't happen in practice)
1227
+ }
1228
+
1229
+ /**
1230
+ * Expands the user-provided path: handles ~ and resolves to absolute.
1231
+ * @param {string} inputPath - User input path.
1232
+ * @returns {string} Absolute expanded path.
1233
+ */
1234
+ expandUserPath(inputPath) {
1235
+ if (typeof inputPath !== 'string') {
1236
+ throw new TypeError('Path must be a string.');
1237
+ }
1238
+
1239
+ let expanded = inputPath.trim();
1240
+
1241
+ // Handle tilde expansion
1242
+ if (expanded.startsWith('~')) {
1243
+ if (expanded === '~') {
1244
+ expanded = os.homedir();
1245
+ } else if (expanded.startsWith('~' + path.sep)) {
1246
+ const pathAfterHome = expanded.slice(2); // Remove ~/ or ~\
1247
+ expanded = path.join(os.homedir(), pathAfterHome);
1248
+ } else {
1249
+ const restOfPath = expanded.slice(1);
1250
+ const separatorIndex = restOfPath.indexOf(path.sep);
1251
+ const username = separatorIndex === -1 ? restOfPath : restOfPath.slice(0, separatorIndex);
1252
+ if (username) {
1253
+ throw new Error(`Path expansion for ~${username} is not supported. Please use an absolute path or ~${path.sep}`);
1254
+ }
1255
+ }
1256
+ }
1257
+
1258
+ // Resolve to the absolute path relative to the current working directory
1259
+ return path.resolve(expanded);
1260
+ }
1261
+
1262
+ /**
1263
+ * Get configured IDEs from existing installation
1264
+ * @param {string} directory - Installation directory
1265
+ * @returns {Array} List of configured IDEs
1266
+ */
1267
+ async getConfiguredIdes(directory) {
1268
+ const { ExistingInstall } = require('./core/existing-install');
1269
+ const { Installer } = require('./core/installer');
1270
+ const installer = new Installer();
1271
+ const { scmDir } = await installer.findBmadDir(directory);
1272
+ const existingInstall = await ExistingInstall.detect(scmDir);
1273
+ return existingInstall.ides;
1274
+ }
1275
+
1276
+ /**
1277
+ * Validate custom content path synchronously
1278
+ * @param {string} input - User input path
1279
+ * @returns {string|undefined} Error message or undefined if valid
1280
+ */
1281
+ validateCustomContentPathSync(input) {
1282
+ // Allow empty input to cancel
1283
+ if (!input || input.trim() === '') {
1284
+ return; // Allow empty to exit
1285
+ }
1286
+
1287
+ try {
1288
+ // Expand the path
1289
+ const expandedPath = this.expandUserPath(input.trim());
1290
+
1291
+ // Check if path exists
1292
+ if (!fs.pathExistsSync(expandedPath)) {
1293
+ return 'Path does not exist';
1294
+ }
1295
+
1296
+ // Check if it's a directory
1297
+ const stat = fs.statSync(expandedPath);
1298
+ if (!stat.isDirectory()) {
1299
+ return 'Path must be a directory';
1300
+ }
1301
+
1302
+ // Check for module.yaml in the root
1303
+ const moduleYamlPath = path.join(expandedPath, 'module.yaml');
1304
+ if (!fs.pathExistsSync(moduleYamlPath)) {
1305
+ return 'Directory must contain a module.yaml file in the root';
1306
+ }
1307
+
1308
+ // Try to parse the module.yaml to get the module ID
1309
+ try {
1310
+ const yaml = require('yaml');
1311
+ const content = fs.readFileSync(moduleYamlPath, 'utf8');
1312
+ const moduleData = yaml.parse(content);
1313
+ if (!moduleData.code) {
1314
+ return 'module.yaml must contain a "code" field for the module ID';
1315
+ }
1316
+ } catch (error) {
1317
+ return 'Invalid module.yaml file: ' + error.message;
1318
+ }
1319
+
1320
+ return; // Valid
1321
+ } catch (error) {
1322
+ return 'Error validating path: ' + error.message;
1323
+ }
1324
+ }
1325
+
1326
+ /**
1327
+ * Prompt user for custom content source location
1328
+ * @returns {Object} Custom content configuration
1329
+ */
1330
+ async promptCustomContentSource() {
1331
+ const customContentConfig = { hasCustomContent: true, sources: [] };
1332
+
1333
+ // Keep asking for more sources until user is done
1334
+ while (true) {
1335
+ // First ask if user wants to add another module or continue
1336
+ if (customContentConfig.sources.length > 0) {
1337
+ const action = await prompts.select({
1338
+ message: 'Would you like to:',
1339
+ choices: [
1340
+ { name: 'Add another custom module', value: 'add' },
1341
+ { name: 'Continue with installation', value: 'continue' },
1342
+ ],
1343
+ default: 'continue',
1344
+ });
1345
+
1346
+ if (action === 'continue') {
1347
+ break;
1348
+ }
1349
+ }
1350
+
1351
+ let sourcePath;
1352
+ let isValid = false;
1353
+
1354
+ while (!isValid) {
1355
+ // Use sync validation because @clack/prompts doesn't support async validate
1356
+ const inputPath = await prompts.text({
1357
+ message: 'Path to custom module folder (press Enter to skip):',
1358
+ validate: (input) => this.validateCustomContentPathSync(input),
1359
+ });
1360
+
1361
+ // If user pressed Enter without typing anything, exit the loop
1362
+ if (!inputPath || inputPath.trim() === '') {
1363
+ // If we have no modules yet, return false for no custom content
1364
+ if (customContentConfig.sources.length === 0) {
1365
+ return { hasCustomContent: false };
1366
+ }
1367
+ return customContentConfig;
1368
+ }
1369
+
1370
+ sourcePath = this.expandUserPath(inputPath);
1371
+ isValid = true;
1372
+ }
1373
+
1374
+ // Read module.yaml to get module info
1375
+ const yaml = require('yaml');
1376
+ const moduleYamlPath = path.join(sourcePath, 'module.yaml');
1377
+ const moduleContent = await fs.readFile(moduleYamlPath, 'utf8');
1378
+ const moduleData = yaml.parse(moduleContent);
1379
+
1380
+ // Add to sources
1381
+ customContentConfig.sources.push({
1382
+ path: sourcePath,
1383
+ id: moduleData.code,
1384
+ name: moduleData.name || moduleData.code,
1385
+ });
1386
+
1387
+ await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`);
1388
+ }
1389
+
1390
+ // Ask if user wants to add these to the installation
1391
+ const shouldInstall = await prompts.confirm({
1392
+ message: `Install these ${customContentConfig.sources.length} custom modules?`,
1393
+ default: true,
1394
+ });
1395
+
1396
+ if (shouldInstall) {
1397
+ customContentConfig.selected = true;
1398
+ // Store paths to module.yaml files, not directories
1399
+ customContentConfig.selectedFiles = customContentConfig.sources.map((s) => path.join(s.path, 'module.yaml'));
1400
+ // Also include module IDs for installation
1401
+ customContentConfig.selectedModuleIds = customContentConfig.sources.map((s) => s.id);
1402
+ }
1403
+
1404
+ return customContentConfig;
1405
+ }
1406
+
1407
+ /**
1408
+ * Handle custom modules in the modify flow
1409
+ * @param {string} directory - Installation directory
1410
+ * @param {Array} selectedModules - Currently selected modules
1411
+ * @returns {Object} Result with selected custom modules and custom content config
1412
+ */
1413
+ async handleCustomModulesInModifyFlow(directory, selectedModules) {
1414
+ // Get existing installation to find custom modules
1415
+ const { existingInstall } = await this.getExistingInstallation(directory);
1416
+
1417
+ // Check if there are any custom modules in cache
1418
+ const { Installer } = require('./core/installer');
1419
+ const installer = new Installer();
1420
+ const { scmDir } = await installer.findBmadDir(directory);
1421
+
1422
+ const cacheDir = path.join(scmDir, '_config', 'custom');
1423
+ const cachedCustomModules = [];
1424
+
1425
+ if (await fs.pathExists(cacheDir)) {
1426
+ const entries = await fs.readdir(cacheDir, { withFileTypes: true });
1427
+ for (const entry of entries) {
1428
+ if (entry.isDirectory()) {
1429
+ const moduleYamlPath = path.join(cacheDir, entry.name, 'module.yaml');
1430
+ if (await fs.pathExists(moduleYamlPath)) {
1431
+ const yaml = require('yaml');
1432
+ const content = await fs.readFile(moduleYamlPath, 'utf8');
1433
+ const moduleData = yaml.parse(content);
1434
+
1435
+ cachedCustomModules.push({
1436
+ id: entry.name,
1437
+ name: moduleData.name || entry.name,
1438
+ description: moduleData.description || 'Custom module from cache',
1439
+ checked: selectedModules.includes(entry.name),
1440
+ fromCache: true,
1441
+ });
1442
+ }
1443
+ }
1444
+ }
1445
+ }
1446
+
1447
+ const result = {
1448
+ selectedCustomModules: [],
1449
+ customContentConfig: { hasCustomContent: false },
1450
+ };
1451
+
1452
+ // Ask user about custom modules
1453
+ await prompts.log.info('Custom Modules');
1454
+ if (cachedCustomModules.length > 0) {
1455
+ await prompts.log.message('Found custom modules in your installation:');
1456
+ } else {
1457
+ await prompts.log.message('No custom modules currently installed.');
1458
+ }
1459
+
1460
+ // Build choices dynamically based on whether we have existing modules
1461
+ const choices = [];
1462
+ if (cachedCustomModules.length > 0) {
1463
+ choices.push(
1464
+ { name: 'Keep all existing custom modules', value: 'keep' },
1465
+ { name: 'Select which custom modules to keep', value: 'select' },
1466
+ { name: 'Add new custom modules', value: 'add' },
1467
+ { name: 'Remove all custom modules', value: 'remove' },
1468
+ );
1469
+ } else {
1470
+ choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
1471
+ }
1472
+
1473
+ const customAction = await prompts.select({
1474
+ message: cachedCustomModules.length > 0 ? 'Manage custom modules?' : 'Add custom modules?',
1475
+ choices: choices,
1476
+ default: cachedCustomModules.length > 0 ? 'keep' : 'add',
1477
+ });
1478
+
1479
+ switch (customAction) {
1480
+ case 'keep': {
1481
+ // Keep all existing custom modules
1482
+ result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
1483
+ await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`);
1484
+ break;
1485
+ }
1486
+
1487
+ case 'select': {
1488
+ // Let user choose which to keep
1489
+ const selectChoices = cachedCustomModules.map((m) => ({
1490
+ name: `${m.name} (${m.id})`,
1491
+ value: m.id,
1492
+ checked: m.checked,
1493
+ }));
1494
+
1495
+ // Add "None / I changed my mind" option at the end
1496
+ const choicesWithSkip = [
1497
+ ...selectChoices,
1498
+ {
1499
+ name: '⚠ None / I changed my mind - keep no custom modules',
1500
+ value: '__NONE__',
1501
+ checked: false,
1502
+ },
1503
+ ];
1504
+
1505
+ const keepModules = await prompts.multiselect({
1506
+ message: 'Select custom modules to keep (use arrow keys, space to toggle):',
1507
+ choices: choicesWithSkip,
1508
+ required: true,
1509
+ });
1510
+
1511
+ // If user selected both "__NONE__" and other modules, honor the "None" choice
1512
+ if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) {
1513
+ await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.');
1514
+ result.selectedCustomModules = [];
1515
+ } else {
1516
+ // Filter out the special '__NONE__' value
1517
+ result.selectedCustomModules = keepModules ? keepModules.filter((m) => m !== '__NONE__') : [];
1518
+ }
1519
+ break;
1520
+ }
1521
+
1522
+ case 'add': {
1523
+ // By default, keep existing modules when adding new ones
1524
+ // User chose "Add new" not "Replace", so we assume they want to keep existing
1525
+ result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
1526
+
1527
+ // Then prompt for new ones (reuse existing method)
1528
+ const newCustomContent = await this.promptCustomContentSource();
1529
+ if (newCustomContent.hasCustomContent && newCustomContent.selected) {
1530
+ result.selectedCustomModules.push(...newCustomContent.selectedModuleIds);
1531
+ result.customContentConfig = newCustomContent;
1532
+ }
1533
+ break;
1534
+ }
1535
+
1536
+ case 'remove': {
1537
+ // Remove all custom modules
1538
+ await prompts.log.warn('All custom modules will be removed from the installation');
1539
+ break;
1540
+ }
1541
+
1542
+ case 'cancel': {
1543
+ // User cancelled - no custom modules
1544
+ await prompts.log.message('No custom modules will be added');
1545
+ break;
1546
+ }
1547
+ }
1548
+
1549
+ return result;
1550
+ }
1551
+
1552
+ /**
1553
+ * Display module versions with update availability
1554
+ * @param {Array} modules - Array of module info objects with version info
1555
+ * @param {Array} availableUpdates - Array of available updates
1556
+ */
1557
+ async displayModuleVersions(modules, availableUpdates = []) {
1558
+ // Group modules by source
1559
+ const builtIn = modules.filter((m) => m.source === 'built-in');
1560
+ const external = modules.filter((m) => m.source === 'external');
1561
+ const custom = modules.filter((m) => m.source === 'custom');
1562
+ const unknown = modules.filter((m) => m.source === 'unknown');
1563
+
1564
+ const lines = [];
1565
+ const formatGroup = (group, title) => {
1566
+ if (group.length === 0) return;
1567
+ lines.push(title);
1568
+ for (const mod of group) {
1569
+ const updateInfo = availableUpdates.find((u) => u.name === mod.name);
1570
+ const versionDisplay = mod.version || 'unknown';
1571
+ if (updateInfo) {
1572
+ lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2192 ${updateInfo.latestVersion} \u2191`);
1573
+ } else {
1574
+ lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2713`);
1575
+ }
1576
+ }
1577
+ };
1578
+
1579
+ formatGroup(builtIn, 'Built-in Modules');
1580
+ formatGroup(external, 'External Modules (Official)');
1581
+ formatGroup(custom, 'Custom Modules');
1582
+ formatGroup(unknown, 'Other Modules');
1583
+
1584
+ await prompts.note(lines.join('\n'), 'Module Versions');
1585
+ }
1586
+
1587
+ /**
1588
+ * Prompt user to select which modules to update
1589
+ * @param {Array} availableUpdates - Array of available updates
1590
+ * @returns {Array} Selected module names to update
1591
+ */
1592
+ async promptUpdateSelection(availableUpdates) {
1593
+ if (availableUpdates.length === 0) {
1594
+ return [];
1595
+ }
1596
+
1597
+ await prompts.log.info('Available Updates');
1598
+
1599
+ const choices = availableUpdates.map((update) => ({
1600
+ name: `${update.name} (v${update.installedVersion} \u2192 v${update.latestVersion})`,
1601
+ value: update.name,
1602
+ checked: true, // Default to selecting all updates
1603
+ }));
1604
+
1605
+ // Add "Update All" and "Cancel" options
1606
+ const action = await prompts.select({
1607
+ message: 'How would you like to proceed?',
1608
+ choices: [
1609
+ { name: 'Update all available modules', value: 'all' },
1610
+ { name: 'Select specific modules to update', value: 'select' },
1611
+ { name: 'Skip updates for now', value: 'skip' },
1612
+ ],
1613
+ default: 'all',
1614
+ });
1615
+
1616
+ if (action === 'all') {
1617
+ return availableUpdates.map((u) => u.name);
1618
+ }
1619
+
1620
+ if (action === 'skip') {
1621
+ return [];
1622
+ }
1623
+
1624
+ // Allow specific selection
1625
+ const selected = await prompts.multiselect({
1626
+ message: 'Select modules to update (use arrow keys, space to toggle):',
1627
+ choices: choices,
1628
+ required: true,
1629
+ });
1630
+
1631
+ return selected || [];
1632
+ }
1633
+
1634
+ /**
1635
+ * Display status of all installed modules
1636
+ * @param {Object} statusData - Status data with modules, installation info, and available updates
1637
+ */
1638
+ async displayStatus(statusData) {
1639
+ const { installation, modules, availableUpdates, scmDir } = statusData;
1640
+
1641
+ // Installation info
1642
+ const infoLines = [
1643
+ `Version: ${installation.version || 'unknown'}`,
1644
+ `Location: ${scmDir}`,
1645
+ `Installed: ${new Date(installation.installDate).toLocaleDateString()}`,
1646
+ `Last Updated: ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : 'unknown'}`,
1647
+ ];
1648
+
1649
+ await prompts.note(infoLines.join('\n'), 'SCM Status');
1650
+
1651
+ // Module versions
1652
+ await this.displayModuleVersions(modules, availableUpdates);
1653
+
1654
+ // Update summary
1655
+ if (availableUpdates.length > 0) {
1656
+ await prompts.log.warn(`${availableUpdates.length} update(s) available`);
1657
+ await prompts.log.message('Run \'scm install\' and select "Quick Update" to update');
1658
+ } else {
1659
+ await prompts.log.success('All modules are up to date');
1660
+ }
1661
+ }
1662
+
1663
+ /**
1664
+ * Display list of selected tools after IDE selection
1665
+ * @param {Array} selectedIdes - Array of selected IDE values
1666
+ * @param {Array} preferredIdes - Array of preferred IDE objects
1667
+ * @param {Array} allTools - Array of all tool objects
1668
+ */
1669
+ async displaySelectedTools(selectedIdes, preferredIdes, allTools) {
1670
+ if (selectedIdes.length === 0) return;
1671
+
1672
+ const preferredValues = new Set(preferredIdes.map((ide) => ide.value));
1673
+ const toolLines = selectedIdes.map((ideValue) => {
1674
+ const tool = allTools.find((t) => t.value === ideValue);
1675
+ const name = tool?.name || ideValue;
1676
+ const marker = preferredValues.has(ideValue) ? ' \u2B50' : '';
1677
+ return ` \u2022 ${name}${marker}`;
1678
+ });
1679
+ await prompts.log.message('Selected tools:\n' + toolLines.join('\n'));
1680
+ }
1681
+ }
1682
+
1683
+ module.exports = { UI };