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,2043 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const yaml = require('yaml');
4
+ const prompts = require('../prompts');
5
+ const { getProjectRoot, getSourcePath, getModulePath } = require('../project-root');
6
+ const { CLIUtils } = require('../cli-utils');
7
+ const { ExternalModuleManager } = require('./external-manager');
8
+
9
+ class OfficialModules {
10
+ constructor(options = {}) {
11
+ this.externalModuleManager = new ExternalModuleManager();
12
+ // Config collection state (merged from ConfigCollector)
13
+ this.collectedConfig = {};
14
+ this._existingConfig = null;
15
+ this.currentProjectDir = null;
16
+ }
17
+
18
+ /**
19
+ * Module configurations collected during install.
20
+ */
21
+ get moduleConfigs() {
22
+ return this.collectedConfig;
23
+ }
24
+
25
+ /**
26
+ * Existing module configurations read from a previous installation.
27
+ */
28
+ get existingConfig() {
29
+ return this._existingConfig;
30
+ }
31
+
32
+ /**
33
+ * Build a configured OfficialModules instance from install config.
34
+ * @param {Object} config - Clean install config (from Config.build)
35
+ * @param {Object} paths - InstallPaths instance
36
+ * @returns {OfficialModules}
37
+ */
38
+ static async build(config, paths) {
39
+ const instance = new OfficialModules();
40
+
41
+ // Pre-collected by UI or quickUpdate — store and load existing for path-change detection
42
+ if (config.moduleConfigs) {
43
+ instance.collectedConfig = config.moduleConfigs;
44
+ await instance.loadExistingConfig(paths.projectRoot);
45
+ return instance;
46
+ }
47
+
48
+ // Headless collection (--yes flag from CLI without UI, tests)
49
+ if (config.hasCoreConfig()) {
50
+ instance.collectedConfig.core = config.coreConfig;
51
+ instance.allAnswers = {};
52
+ for (const [key, value] of Object.entries(config.coreConfig)) {
53
+ instance.allAnswers[`core_${key}`] = value;
54
+ }
55
+ }
56
+
57
+ const toCollect = config.hasCoreConfig() ? config.modules.filter((m) => m !== 'core') : [...config.modules];
58
+
59
+ await instance.collectAllConfigurations(toCollect, paths.projectRoot, {
60
+ skipPrompts: config.skipPrompts,
61
+ });
62
+
63
+ return instance;
64
+ }
65
+
66
+ /**
67
+ * Copy a file to the target location
68
+ * @param {string} sourcePath - Source file path
69
+ * @param {string} targetPath - Target file path
70
+ * @param {boolean} overwrite - Whether to overwrite existing files (default: true)
71
+ */
72
+ async copyFile(sourcePath, targetPath, overwrite = true) {
73
+ await fs.copy(sourcePath, targetPath, { overwrite });
74
+ }
75
+
76
+ /**
77
+ * Copy a directory recursively
78
+ * @param {string} sourceDir - Source directory path
79
+ * @param {string} targetDir - Target directory path
80
+ * @param {boolean} overwrite - Whether to overwrite existing files (default: true)
81
+ */
82
+ async copyDirectory(sourceDir, targetDir, overwrite = true) {
83
+ await fs.ensureDir(targetDir);
84
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
85
+
86
+ for (const entry of entries) {
87
+ const sourcePath = path.join(sourceDir, entry.name);
88
+ const targetPath = path.join(targetDir, entry.name);
89
+
90
+ if (entry.isDirectory()) {
91
+ await this.copyDirectory(sourcePath, targetPath, overwrite);
92
+ } else {
93
+ await this.copyFile(sourcePath, targetPath, overwrite);
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * List all available built-in modules (core and bmm).
100
+ * All other modules come from external-official-modules.yaml
101
+ * @returns {Object} Object with modules array and customModules array
102
+ */
103
+ async listAvailable() {
104
+ const modules = [];
105
+ const customModules = [];
106
+
107
+ // Add built-in core module (directly under src/core-skills)
108
+ const corePath = getSourcePath('core-skills');
109
+ if (await fs.pathExists(corePath)) {
110
+ const coreInfo = await this.getModuleInfo(corePath, 'core', 'src/core-skills');
111
+ if (coreInfo) {
112
+ modules.push(coreInfo);
113
+ }
114
+ }
115
+
116
+ // Add built-in bmm module (directly under src/bmm-skills)
117
+ const bmmPath = getSourcePath('bmm-skills');
118
+ if (await fs.pathExists(bmmPath)) {
119
+ const bmmInfo = await this.getModuleInfo(bmmPath, 'bmm', 'src/bmm-skills');
120
+ if (bmmInfo) {
121
+ modules.push(bmmInfo);
122
+ }
123
+ }
124
+
125
+ return { modules, customModules };
126
+ }
127
+
128
+ /**
129
+ * Get module information from a module path
130
+ * @param {string} modulePath - Path to the module directory
131
+ * @param {string} defaultName - Default name for the module
132
+ * @param {string} sourceDescription - Description of where the module was found
133
+ * @returns {Object|null} Module info or null if not a valid module
134
+ */
135
+ async getModuleInfo(modulePath, defaultName, sourceDescription) {
136
+ // Check for module structure (module.yaml OR custom.yaml)
137
+ const moduleConfigPath = path.join(modulePath, 'module.yaml');
138
+ const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
139
+ let configPath = null;
140
+
141
+ if (await fs.pathExists(moduleConfigPath)) {
142
+ configPath = moduleConfigPath;
143
+ } else if (await fs.pathExists(rootCustomConfigPath)) {
144
+ configPath = rootCustomConfigPath;
145
+ }
146
+
147
+ // Skip if this doesn't look like a module
148
+ if (!configPath) {
149
+ return null;
150
+ }
151
+
152
+ // Mark as custom if it's using custom.yaml OR if it's outside src/bmm or src/core
153
+ const isCustomSource =
154
+ sourceDescription !== 'src/bmm-skills' && sourceDescription !== 'src/core-skills' && sourceDescription !== 'src/modules';
155
+ const moduleInfo = {
156
+ id: defaultName,
157
+ path: modulePath,
158
+ name: defaultName
159
+ .split('-')
160
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
161
+ .join(' '),
162
+ description: 'SCM Module',
163
+ version: '5.0.0',
164
+ source: sourceDescription,
165
+ isCustom: configPath === rootCustomConfigPath || isCustomSource,
166
+ };
167
+
168
+ // Read module config for metadata
169
+ try {
170
+ const configContent = await fs.readFile(configPath, 'utf8');
171
+ const config = yaml.parse(configContent);
172
+
173
+ // Use the code property as the id if available
174
+ if (config.code) {
175
+ moduleInfo.id = config.code;
176
+ }
177
+
178
+ moduleInfo.name = config.name || moduleInfo.name;
179
+ moduleInfo.description = config.description || moduleInfo.description;
180
+ moduleInfo.version = config.version || moduleInfo.version;
181
+ moduleInfo.dependencies = config.dependencies || [];
182
+ moduleInfo.defaultSelected = config.default_selected === undefined ? false : config.default_selected;
183
+ } catch (error) {
184
+ await prompts.log.warn(`Failed to read config for ${defaultName}: ${error.message}`);
185
+ }
186
+
187
+ return moduleInfo;
188
+ }
189
+
190
+ /**
191
+ * Find the source path for a module by searching all possible locations
192
+ * @param {string} moduleCode - Code of the module to find (from module.yaml)
193
+ * @returns {string|null} Path to the module source or null if not found
194
+ */
195
+ async findModuleSource(moduleCode, options = {}) {
196
+ const projectRoot = getProjectRoot();
197
+
198
+ // Check for core module (directly under src/core-skills)
199
+ if (moduleCode === 'core') {
200
+ const corePath = getSourcePath('core-skills');
201
+ if (await fs.pathExists(corePath)) {
202
+ return corePath;
203
+ }
204
+ }
205
+
206
+ // Check for built-in bmm module (directly under src/bmm-skills)
207
+ if (moduleCode === 'bmm') {
208
+ const bmmPath = getSourcePath('bmm-skills');
209
+ if (await fs.pathExists(bmmPath)) {
210
+ return bmmPath;
211
+ }
212
+ }
213
+
214
+ // Check external official modules
215
+ const externalSource = await this.externalModuleManager.findExternalModuleSource(moduleCode, options);
216
+ if (externalSource) {
217
+ return externalSource;
218
+ }
219
+
220
+ return null;
221
+ }
222
+
223
+ /**
224
+ * Install a module
225
+ * @param {string} moduleName - Code of the module to install (from module.yaml)
226
+ * @param {string} scmDir - Target scm directory
227
+ * @param {Function} fileTrackingCallback - Optional callback to track installed files
228
+ * @param {Object} options - Additional installation options
229
+ * @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
230
+ * @param {Object} options.moduleConfig - Module configuration from config collector
231
+ * @param {Object} options.logger - Logger instance for output
232
+ */
233
+ async install(moduleName, scmDir, fileTrackingCallback = null, options = {}) {
234
+ const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
235
+ const targetPath = path.join(scmDir, moduleName);
236
+
237
+ if (!sourcePath) {
238
+ throw new Error(
239
+ `Source for module '${moduleName}' is not available. It will be retained but cannot be updated without its source files.`,
240
+ );
241
+ }
242
+
243
+ if (await fs.pathExists(targetPath)) {
244
+ await fs.remove(targetPath);
245
+ }
246
+
247
+ await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig);
248
+
249
+ if (!options.skipModuleInstaller) {
250
+ await this.createModuleDirectories(moduleName, scmDir, options);
251
+ }
252
+
253
+ const { Manifest } = require('../core/manifest');
254
+ const manifestObj = new Manifest();
255
+ const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, scmDir, sourcePath);
256
+
257
+ await manifestObj.addModule(scmDir, moduleName, {
258
+ version: versionInfo.version,
259
+ source: versionInfo.source,
260
+ npmPackage: versionInfo.npmPackage,
261
+ repoUrl: versionInfo.repoUrl,
262
+ });
263
+
264
+ return { success: true, module: moduleName, path: targetPath, versionInfo };
265
+ }
266
+
267
+ /**
268
+ * Update an existing module
269
+ * @param {string} moduleName - Name of the module to update
270
+ * @param {string} scmDir - Target scm directory
271
+ */
272
+ async update(moduleName, scmDir) {
273
+ const sourcePath = await this.findModuleSource(moduleName);
274
+ const targetPath = path.join(scmDir, moduleName);
275
+
276
+ if (!sourcePath) {
277
+ throw new Error(`Module '${moduleName}' not found in any source location`);
278
+ }
279
+
280
+ if (!(await fs.pathExists(targetPath))) {
281
+ throw new Error(`Module '${moduleName}' is not installed`);
282
+ }
283
+
284
+ await this.syncModule(sourcePath, targetPath);
285
+
286
+ return {
287
+ success: true,
288
+ module: moduleName,
289
+ path: targetPath,
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Remove a module
295
+ * @param {string} moduleName - Name of the module to remove
296
+ * @param {string} scmDir - Target scm directory
297
+ */
298
+ async remove(moduleName, scmDir) {
299
+ const targetPath = path.join(scmDir, moduleName);
300
+
301
+ if (!(await fs.pathExists(targetPath))) {
302
+ throw new Error(`Module '${moduleName}' is not installed`);
303
+ }
304
+
305
+ await fs.remove(targetPath);
306
+
307
+ return {
308
+ success: true,
309
+ module: moduleName,
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Check if a module is installed
315
+ * @param {string} moduleName - Name of the module
316
+ * @param {string} scmDir - Target scm directory
317
+ * @returns {boolean} True if module is installed
318
+ */
319
+ async isInstalled(moduleName, scmDir) {
320
+ const targetPath = path.join(scmDir, moduleName);
321
+ return await fs.pathExists(targetPath);
322
+ }
323
+
324
+ /**
325
+ * Get installed module info
326
+ * @param {string} moduleName - Name of the module
327
+ * @param {string} scmDir - Target scm directory
328
+ * @returns {Object|null} Module info or null if not installed
329
+ */
330
+ async getInstalledInfo(moduleName, scmDir) {
331
+ const targetPath = path.join(scmDir, moduleName);
332
+
333
+ if (!(await fs.pathExists(targetPath))) {
334
+ return null;
335
+ }
336
+
337
+ const configPath = path.join(targetPath, 'config.yaml');
338
+ const moduleInfo = {
339
+ id: moduleName,
340
+ path: targetPath,
341
+ installed: true,
342
+ };
343
+
344
+ if (await fs.pathExists(configPath)) {
345
+ try {
346
+ const configContent = await fs.readFile(configPath, 'utf8');
347
+ const config = yaml.parse(configContent);
348
+ Object.assign(moduleInfo, config);
349
+ } catch (error) {
350
+ await prompts.log.warn(`Failed to read installed module config: ${error.message}`);
351
+ }
352
+ }
353
+
354
+ return moduleInfo;
355
+ }
356
+
357
+ /**
358
+ * Copy module with filtering for localskip agents and conditional content
359
+ * @param {string} sourcePath - Source module path
360
+ * @param {string} targetPath - Target module path
361
+ * @param {Function} fileTrackingCallback - Optional callback to track installed files
362
+ * @param {Object} moduleConfig - Module configuration with conditional flags
363
+ */
364
+ async copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback = null, moduleConfig = {}) {
365
+ // Get all files in source
366
+ const sourceFiles = await this.getFileList(sourcePath);
367
+
368
+ for (const file of sourceFiles) {
369
+ // Skip sub-modules directory - these are IDE-specific and handled separately
370
+ if (file.startsWith('sub-modules/')) {
371
+ continue;
372
+ }
373
+
374
+ // Skip sidecar directories - these contain agent-specific assets not needed at install time
375
+ const isInSidecarDirectory = path
376
+ .dirname(file)
377
+ .split('/')
378
+ .some((dir) => dir.toLowerCase().endsWith('-sidecar'));
379
+
380
+ if (isInSidecarDirectory) {
381
+ continue;
382
+ }
383
+
384
+ // Skip module.yaml at root - it's only needed at install time
385
+ if (file === 'module.yaml') {
386
+ continue;
387
+ }
388
+
389
+ // Skip module root config.yaml only - generated by config collector with actual values
390
+ // Workflow-level config.yaml (e.g. workflows/orchestrate-story/config.yaml) must be copied
391
+ // for custom modules that use workflow-specific configuration
392
+ if (file === 'config.yaml') {
393
+ continue;
394
+ }
395
+
396
+ const sourceFile = path.join(sourcePath, file);
397
+ const targetFile = path.join(targetPath, file);
398
+
399
+ // Check if this is an agent file
400
+ if (file.startsWith('agents/') && file.endsWith('.md')) {
401
+ // Read the file to check for localskip
402
+ const content = await fs.readFile(sourceFile, 'utf8');
403
+
404
+ // Check for localskip="true" in the agent tag
405
+ const agentMatch = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
406
+ if (agentMatch) {
407
+ await prompts.log.message(` Skipping web-only agent: ${path.basename(file)}`);
408
+ continue; // Skip this agent
409
+ }
410
+ }
411
+
412
+ // Copy the file with placeholder replacement
413
+ await this.copyFile(sourceFile, targetFile);
414
+
415
+ // Track the file if callback provided
416
+ if (fileTrackingCallback) {
417
+ fileTrackingCallback(targetFile);
418
+ }
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Find all .md agent files recursively in a directory
424
+ * @param {string} dir - Directory to search
425
+ * @returns {Array} List of .md agent file paths
426
+ */
427
+ async findAgentMdFiles(dir) {
428
+ const agentFiles = [];
429
+
430
+ async function searchDirectory(searchDir) {
431
+ const entries = await fs.readdir(searchDir, { withFileTypes: true });
432
+
433
+ for (const entry of entries) {
434
+ const fullPath = path.join(searchDir, entry.name);
435
+
436
+ if (entry.isFile() && entry.name.endsWith('.md')) {
437
+ agentFiles.push(fullPath);
438
+ } else if (entry.isDirectory()) {
439
+ await searchDirectory(fullPath);
440
+ }
441
+ }
442
+ }
443
+
444
+ await searchDirectory(dir);
445
+ return agentFiles;
446
+ }
447
+
448
+ /**
449
+ * Create directories declared in module.yaml's `directories` key
450
+ * This replaces the security-risky module installer pattern with declarative config
451
+ * During updates, if a directory path changed, moves the old directory to the new path
452
+ * @param {string} moduleName - Name of the module
453
+ * @param {string} scmDir - Target scm directory
454
+ * @param {Object} options - Installation options
455
+ * @param {Object} options.moduleConfig - Module configuration from config collector
456
+ * @param {Object} options.existingModuleConfig - Previous module config (for detecting path changes during updates)
457
+ * @param {Object} options.coreConfig - Core configuration
458
+ * @returns {Promise<{createdDirs: string[], movedDirs: string[], createdWdsFolders: string[]}>} Created directories info
459
+ */
460
+ async createModuleDirectories(moduleName, scmDir, options = {}) {
461
+ const moduleConfig = options.moduleConfig || {};
462
+ const existingModuleConfig = options.existingModuleConfig || {};
463
+ const projectRoot = path.dirname(scmDir);
464
+ const emptyResult = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
465
+
466
+ // Special handling for core module - it's in src/core-skills not src/modules
467
+ let sourcePath;
468
+ if (moduleName === 'core') {
469
+ sourcePath = getSourcePath('core-skills');
470
+ } else {
471
+ sourcePath = await this.findModuleSource(moduleName, { silent: true });
472
+ if (!sourcePath) {
473
+ return emptyResult; // No source found, skip
474
+ }
475
+ }
476
+
477
+ // Read module.yaml to find the `directories` key
478
+ const moduleYamlPath = path.join(sourcePath, 'module.yaml');
479
+ if (!(await fs.pathExists(moduleYamlPath))) {
480
+ return emptyResult; // No module.yaml, skip
481
+ }
482
+
483
+ let moduleYaml;
484
+ try {
485
+ const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
486
+ moduleYaml = yaml.parse(yamlContent);
487
+ } catch (error) {
488
+ await prompts.log.warn(`Invalid module.yaml for ${moduleName}: ${error.message}`);
489
+ return emptyResult;
490
+ }
491
+
492
+ if (!moduleYaml || !moduleYaml.directories) {
493
+ return emptyResult; // No directories declared, skip
494
+ }
495
+
496
+ const directories = moduleYaml.directories;
497
+ const wdsFolders = moduleYaml.wds_folders || [];
498
+ const createdDirs = [];
499
+ const movedDirs = [];
500
+ const createdWdsFolders = [];
501
+
502
+ for (const dirRef of directories) {
503
+ // Parse variable reference like "{design_artifacts}"
504
+ const varMatch = dirRef.match(/^\{([^}]+)\}$/);
505
+ if (!varMatch) {
506
+ // Not a variable reference, skip
507
+ continue;
508
+ }
509
+
510
+ const configKey = varMatch[1];
511
+ const dirValue = moduleConfig[configKey];
512
+ if (!dirValue || typeof dirValue !== 'string') {
513
+ continue; // No value or not a string, skip
514
+ }
515
+
516
+ // Strip {project-root}/ prefix if present
517
+ let dirPath = dirValue.replace(/^\{project-root\}\/?/, '');
518
+
519
+ // Handle remaining {project-root} anywhere in the path
520
+ dirPath = dirPath.replaceAll('{project-root}', '');
521
+
522
+ // Resolve to absolute path
523
+ const fullPath = path.join(projectRoot, dirPath);
524
+
525
+ // Validate path is within project root (prevent directory traversal)
526
+ const normalizedPath = path.normalize(fullPath);
527
+ const normalizedRoot = path.normalize(projectRoot);
528
+ if (!normalizedPath.startsWith(normalizedRoot + path.sep) && normalizedPath !== normalizedRoot) {
529
+ const color = await prompts.getColor();
530
+ await prompts.log.warn(color.yellow(`${configKey} path escapes project root, skipping: ${dirPath}`));
531
+ continue;
532
+ }
533
+
534
+ // Check if directory path changed from previous config (update/modify scenario)
535
+ const oldDirValue = existingModuleConfig[configKey];
536
+ let oldFullPath = null;
537
+ let oldDirPath = null;
538
+ if (oldDirValue && typeof oldDirValue === 'string') {
539
+ // F3: Normalize both values before comparing to avoid false negatives
540
+ // from trailing slashes, separator differences, or prefix format variations
541
+ let normalizedOld = oldDirValue.replace(/^\{project-root\}\/?/, '');
542
+ normalizedOld = path.normalize(normalizedOld.replaceAll('{project-root}', ''));
543
+ const normalizedNew = path.normalize(dirPath);
544
+
545
+ if (normalizedOld !== normalizedNew) {
546
+ oldDirPath = normalizedOld;
547
+ oldFullPath = path.join(projectRoot, oldDirPath);
548
+ const normalizedOldAbsolute = path.normalize(oldFullPath);
549
+ if (!normalizedOldAbsolute.startsWith(normalizedRoot + path.sep) && normalizedOldAbsolute !== normalizedRoot) {
550
+ oldFullPath = null; // Old path escapes project root, ignore it
551
+ }
552
+
553
+ // F13: Prevent parent/child move (e.g. docs/planning → docs/planning/v2)
554
+ if (oldFullPath) {
555
+ const normalizedNewAbsolute = path.normalize(fullPath);
556
+ if (
557
+ normalizedOldAbsolute.startsWith(normalizedNewAbsolute + path.sep) ||
558
+ normalizedNewAbsolute.startsWith(normalizedOldAbsolute + path.sep)
559
+ ) {
560
+ const color = await prompts.getColor();
561
+ await prompts.log.warn(
562
+ color.yellow(
563
+ `${configKey}: cannot move between parent/child paths (${oldDirPath} / ${dirPath}), creating new directory instead`,
564
+ ),
565
+ );
566
+ oldFullPath = null;
567
+ }
568
+ }
569
+ }
570
+ }
571
+
572
+ const dirName = configKey.replaceAll('_', ' ');
573
+
574
+ if (oldFullPath && (await fs.pathExists(oldFullPath)) && !(await fs.pathExists(fullPath))) {
575
+ // Path changed and old dir exists → move old to new location
576
+ // F1: Use fs.move() instead of fs.rename() for cross-device/volume support
577
+ // F2: Wrap in try/catch — fallback to creating new dir on failure
578
+ try {
579
+ await fs.ensureDir(path.dirname(fullPath));
580
+ await fs.move(oldFullPath, fullPath);
581
+ movedDirs.push(`${dirName}: ${oldDirPath} → ${dirPath}`);
582
+ } catch (moveError) {
583
+ const color = await prompts.getColor();
584
+ await prompts.log.warn(
585
+ color.yellow(
586
+ `Failed to move ${oldDirPath} → ${dirPath}: ${moveError.message}\n Creating new directory instead. Please move contents from the old directory manually.`,
587
+ ),
588
+ );
589
+ await fs.ensureDir(fullPath);
590
+ createdDirs.push(`${dirName}: ${dirPath}`);
591
+ }
592
+ } else if (oldFullPath && (await fs.pathExists(oldFullPath)) && (await fs.pathExists(fullPath))) {
593
+ // F5: Both old and new directories exist — warn user about potential orphaned documents
594
+ const color = await prompts.getColor();
595
+ await prompts.log.warn(
596
+ color.yellow(
597
+ `${dirName}: path changed but both directories exist:\n Old: ${oldDirPath}\n New: ${dirPath}\n Old directory may contain orphaned documents — please review and merge manually.`,
598
+ ),
599
+ );
600
+ } else if (!(await fs.pathExists(fullPath))) {
601
+ // New directory doesn't exist yet → create it
602
+ createdDirs.push(`${dirName}: ${dirPath}`);
603
+ await fs.ensureDir(fullPath);
604
+ }
605
+
606
+ // Create WDS subfolders if this is the design_artifacts directory
607
+ if (configKey === 'design_artifacts' && wdsFolders.length > 0) {
608
+ for (const subfolder of wdsFolders) {
609
+ const subPath = path.join(fullPath, subfolder);
610
+ if (!(await fs.pathExists(subPath))) {
611
+ await fs.ensureDir(subPath);
612
+ createdWdsFolders.push(subfolder);
613
+ }
614
+ }
615
+ }
616
+ }
617
+
618
+ return { createdDirs, movedDirs, createdWdsFolders };
619
+ }
620
+
621
+ /**
622
+ * Private: Process module configuration
623
+ * @param {string} modulePath - Path to installed module
624
+ * @param {string} moduleName - Module name
625
+ */
626
+ async processModuleConfig(modulePath, moduleName) {
627
+ const configPath = path.join(modulePath, 'config.yaml');
628
+
629
+ if (await fs.pathExists(configPath)) {
630
+ try {
631
+ let configContent = await fs.readFile(configPath, 'utf8');
632
+
633
+ // Replace path placeholders
634
+ configContent = configContent.replaceAll('{project-root}', `scm/${moduleName}`);
635
+ configContent = configContent.replaceAll('{module}', moduleName);
636
+
637
+ await fs.writeFile(configPath, configContent, 'utf8');
638
+ } catch (error) {
639
+ await prompts.log.warn(`Failed to process module config: ${error.message}`);
640
+ }
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Private: Sync module files (preserving user modifications)
646
+ * @param {string} sourcePath - Source module path
647
+ * @param {string} targetPath - Target module path
648
+ */
649
+ async syncModule(sourcePath, targetPath) {
650
+ // Get list of all source files
651
+ const sourceFiles = await this.getFileList(sourcePath);
652
+
653
+ for (const file of sourceFiles) {
654
+ const sourceFile = path.join(sourcePath, file);
655
+ const targetFile = path.join(targetPath, file);
656
+
657
+ // Check if target file exists and has been modified
658
+ if (await fs.pathExists(targetFile)) {
659
+ const sourceStats = await fs.stat(sourceFile);
660
+ const targetStats = await fs.stat(targetFile);
661
+
662
+ // Skip if target is newer (user modified)
663
+ if (targetStats.mtime > sourceStats.mtime) {
664
+ continue;
665
+ }
666
+ }
667
+
668
+ // Copy file with placeholder replacement
669
+ await this.copyFile(sourceFile, targetFile);
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Private: Get list of all files in a directory
675
+ * @param {string} dir - Directory path
676
+ * @param {string} baseDir - Base directory for relative paths
677
+ * @returns {Array} List of relative file paths
678
+ */
679
+ async getFileList(dir, baseDir = dir) {
680
+ const files = [];
681
+ const entries = await fs.readdir(dir, { withFileTypes: true });
682
+
683
+ for (const entry of entries) {
684
+ const fullPath = path.join(dir, entry.name);
685
+
686
+ if (entry.isDirectory()) {
687
+ const subFiles = await this.getFileList(fullPath, baseDir);
688
+ files.push(...subFiles);
689
+ } else {
690
+ files.push(path.relative(baseDir, fullPath));
691
+ }
692
+ }
693
+
694
+ return files;
695
+ }
696
+
697
+ // ─── Config collection methods (merged from ConfigCollector) ───
698
+
699
+ /**
700
+ * Find the scm installation directory in a project
701
+ * V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml
702
+ * @param {string} projectDir - Project directory
703
+ * @returns {Promise<string>} Path to scm directory
704
+ */
705
+ async findBmadDir(projectDir) {
706
+ // Check if project directory exists
707
+ if (!(await fs.pathExists(projectDir))) {
708
+ // Project doesn't exist yet, return default
709
+ return path.join(projectDir, 'scm');
710
+ }
711
+
712
+ // V6+ strategy: Look for ANY directory with _config/manifest.yaml
713
+ // This is the definitive marker of a V6+ installation
714
+ try {
715
+ const entries = await fs.readdir(projectDir, { withFileTypes: true });
716
+ for (const entry of entries) {
717
+ if (entry.isDirectory()) {
718
+ const manifestPath = path.join(projectDir, entry.name, '_config', 'manifest.yaml');
719
+ if (await fs.pathExists(manifestPath)) {
720
+ // Found a V6+ installation
721
+ return path.join(projectDir, entry.name);
722
+ }
723
+ }
724
+ }
725
+ } catch {
726
+ // Ignore errors, fall through to default
727
+ }
728
+
729
+ // No V6+ installation found, return default
730
+ // This will be used for new installations
731
+ return path.join(projectDir, 'scm');
732
+ }
733
+
734
+ /**
735
+ * Detect the existing SCM folder name in a project
736
+ * @param {string} projectDir - Project directory
737
+ * @returns {Promise<string|null>} Folder name (just the name, not full path) or null if not found
738
+ */
739
+ async detectExistingBmadFolder(projectDir) {
740
+ // Check if project directory exists
741
+ if (!(await fs.pathExists(projectDir))) {
742
+ return null;
743
+ }
744
+
745
+ // Look for ANY directory with _config/manifest.yaml
746
+ try {
747
+ const entries = await fs.readdir(projectDir, { withFileTypes: true });
748
+ for (const entry of entries) {
749
+ if (entry.isDirectory()) {
750
+ const manifestPath = path.join(projectDir, entry.name, '_config', 'manifest.yaml');
751
+ if (await fs.pathExists(manifestPath)) {
752
+ // Found a V6+ installation, return just the folder name
753
+ return entry.name;
754
+ }
755
+ }
756
+ }
757
+ } catch {
758
+ // Ignore errors
759
+ }
760
+
761
+ return null;
762
+ }
763
+
764
+ /**
765
+ * Load existing config if it exists from module config files
766
+ * @param {string} projectDir - Target project directory
767
+ */
768
+ async loadExistingConfig(projectDir) {
769
+ this._existingConfig = {};
770
+
771
+ // Check if project directory exists first
772
+ if (!(await fs.pathExists(projectDir))) {
773
+ return false;
774
+ }
775
+
776
+ // Find the actual scm directory (handles custom folder names)
777
+ const scmDir = await this.findBmadDir(projectDir);
778
+
779
+ // Check if scm directory exists
780
+ if (!(await fs.pathExists(scmDir))) {
781
+ return false;
782
+ }
783
+
784
+ // Dynamically discover all installed modules by scanning scm directory
785
+ // A directory is a module ONLY if it contains a config.yaml file
786
+ let foundAny = false;
787
+ const entries = await fs.readdir(scmDir, { withFileTypes: true });
788
+
789
+ for (const entry of entries) {
790
+ if (entry.isDirectory()) {
791
+ // Skip the _config directory - it's for system use
792
+ if (entry.name === '_config' || entry.name === '_memory') {
793
+ continue;
794
+ }
795
+
796
+ const moduleConfigPath = path.join(scmDir, entry.name, 'config.yaml');
797
+
798
+ if (await fs.pathExists(moduleConfigPath)) {
799
+ try {
800
+ const content = await fs.readFile(moduleConfigPath, 'utf8');
801
+ const moduleConfig = yaml.parse(content);
802
+ if (moduleConfig) {
803
+ this._existingConfig[entry.name] = moduleConfig;
804
+ foundAny = true;
805
+ }
806
+ } catch {
807
+ // Ignore parse errors for individual modules
808
+ }
809
+ }
810
+ }
811
+ }
812
+
813
+ return foundAny;
814
+ }
815
+
816
+ /**
817
+ * Pre-scan module schemas to gather metadata for the configuration gateway prompt.
818
+ * Returns info about which modules have configurable options.
819
+ * @param {Array} modules - List of non-core module names
820
+ * @returns {Promise<Array>} Array of {moduleName, displayName, questionCount, hasFieldsWithoutDefaults}
821
+ */
822
+ async scanModuleSchemas(modules) {
823
+ const metadataFields = new Set(['code', 'name', 'header', 'subheader', 'default_selected']);
824
+ const results = [];
825
+
826
+ for (const moduleName of modules) {
827
+ // Resolve module.yaml path - custom paths first, then standard location, then OfficialModules search
828
+ let moduleConfigPath = null;
829
+ const customPath = this.customModulePaths?.get(moduleName);
830
+ if (customPath) {
831
+ moduleConfigPath = path.join(customPath, 'module.yaml');
832
+ } else {
833
+ const standardPath = path.join(getModulePath(moduleName), 'module.yaml');
834
+ if (await fs.pathExists(standardPath)) {
835
+ moduleConfigPath = standardPath;
836
+ } else {
837
+ const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
838
+ if (moduleSourcePath) {
839
+ moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
840
+ }
841
+ }
842
+ }
843
+
844
+ if (!moduleConfigPath || !(await fs.pathExists(moduleConfigPath))) {
845
+ continue;
846
+ }
847
+
848
+ try {
849
+ const content = await fs.readFile(moduleConfigPath, 'utf8');
850
+ const moduleConfig = yaml.parse(content);
851
+ if (!moduleConfig) continue;
852
+
853
+ const displayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
854
+ const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
855
+ const questionKeys = configKeys.filter((key) => {
856
+ if (metadataFields.has(key)) return false;
857
+ const item = moduleConfig[key];
858
+ return item && typeof item === 'object' && item.prompt;
859
+ });
860
+
861
+ const hasFieldsWithoutDefaults = questionKeys.some((key) => {
862
+ const item = moduleConfig[key];
863
+ return item.default === undefined || item.default === null || item.default === '';
864
+ });
865
+
866
+ results.push({
867
+ moduleName,
868
+ displayName,
869
+ questionCount: questionKeys.length,
870
+ hasFieldsWithoutDefaults,
871
+ });
872
+ } catch (error) {
873
+ await prompts.log.warn(`Could not read schema for module "${moduleName}": ${error.message}`);
874
+ }
875
+ }
876
+
877
+ return results;
878
+ }
879
+
880
+ /**
881
+ * Collect configuration for all modules
882
+ * @param {Array} modules - List of modules to configure (including 'core')
883
+ * @param {string} projectDir - Target project directory
884
+ * @param {Object} options - Additional options
885
+ * @param {Map} options.customModulePaths - Map of module ID to source path for custom modules
886
+ * @param {boolean} options.skipPrompts - Skip prompts and use defaults (for --yes flag)
887
+ */
888
+ async collectAllConfigurations(modules, projectDir, options = {}) {
889
+ // Store custom module paths for use in collectModuleConfig
890
+ this.customModulePaths = options.customModulePaths || new Map();
891
+ this.skipPrompts = options.skipPrompts || false;
892
+ this.modulesToCustomize = undefined;
893
+ await this.loadExistingConfig(projectDir);
894
+
895
+ // Check if core was already collected (e.g., in early collection phase)
896
+ const coreAlreadyCollected = this.collectedConfig.core && Object.keys(this.collectedConfig.core).length > 0;
897
+
898
+ // If core wasn't already collected, include it
899
+ const allModules = coreAlreadyCollected ? modules.filter((m) => m !== 'core') : ['core', ...modules.filter((m) => m !== 'core')];
900
+
901
+ // Store all answers across modules for cross-referencing
902
+ if (!this.allAnswers) {
903
+ this.allAnswers = {};
904
+ }
905
+
906
+ // Split processing: core first, then gateway, then remaining modules
907
+ const coreModules = allModules.filter((m) => m === 'core');
908
+ const nonCoreModules = allModules.filter((m) => m !== 'core');
909
+
910
+ // Collect core config first (always fully prompted)
911
+ for (const moduleName of coreModules) {
912
+ await this.collectModuleConfig(moduleName, projectDir);
913
+ }
914
+
915
+ // Show batch configuration gateway for non-core modules
916
+ // Scan all non-core module schemas for display names and config metadata
917
+ let scannedModules = [];
918
+ if (!this.skipPrompts && nonCoreModules.length > 0) {
919
+ scannedModules = await this.scanModuleSchemas(nonCoreModules);
920
+ const customizableModules = scannedModules.filter((m) => m.questionCount > 0);
921
+
922
+ if (customizableModules.length > 0) {
923
+ const configMode = await prompts.select({
924
+ message: 'Module configuration',
925
+ choices: [
926
+ { name: 'Express Setup', value: 'express', hint: 'accept all defaults (recommended)' },
927
+ { name: 'Customize', value: 'customize', hint: 'choose modules to configure' },
928
+ ],
929
+ default: 'express',
930
+ });
931
+
932
+ if (configMode === 'customize') {
933
+ const choices = customizableModules.map((m) => ({
934
+ name: `${m.displayName} (${m.questionCount} option${m.questionCount === 1 ? '' : 's'})`,
935
+ value: m.moduleName,
936
+ hint: m.hasFieldsWithoutDefaults ? 'has fields without defaults' : undefined,
937
+ checked: m.hasFieldsWithoutDefaults,
938
+ }));
939
+ const selected = await prompts.multiselect({
940
+ message: 'Select modules to customize:',
941
+ choices,
942
+ required: false,
943
+ });
944
+ this.modulesToCustomize = new Set(selected);
945
+ } else {
946
+ // Express mode: no modules to customize
947
+ this.modulesToCustomize = new Set();
948
+ }
949
+ } else {
950
+ // All non-core modules have zero config - no gateway needed
951
+ this.modulesToCustomize = new Set();
952
+ }
953
+ }
954
+
955
+ // Collect remaining non-core modules
956
+ if (this.modulesToCustomize === undefined) {
957
+ // No gateway was shown (skipPrompts, no non-core modules, or direct call) - process all normally
958
+ for (const moduleName of nonCoreModules) {
959
+ await this.collectModuleConfig(moduleName, projectDir);
960
+ }
961
+ } else {
962
+ // Split into default modules (tasks progress) and customized modules (interactive)
963
+ const defaultModules = nonCoreModules.filter((m) => !this.modulesToCustomize.has(m));
964
+ const customizeModules = nonCoreModules.filter((m) => this.modulesToCustomize.has(m));
965
+
966
+ // Run default modules with a single spinner
967
+ if (defaultModules.length > 0) {
968
+ // Build display name map from all scanned modules for pre-call spinner messages
969
+ const displayNameMap = new Map();
970
+ for (const m of scannedModules) {
971
+ displayNameMap.set(m.moduleName, m.displayName);
972
+ }
973
+
974
+ const configSpinner = await prompts.spinner();
975
+ configSpinner.start('Configuring modules...');
976
+ try {
977
+ for (const moduleName of defaultModules) {
978
+ const displayName = displayNameMap.get(moduleName) || moduleName.toUpperCase();
979
+ configSpinner.message(`Configuring ${displayName}...`);
980
+ try {
981
+ this._silentConfig = true;
982
+ await this.collectModuleConfig(moduleName, projectDir);
983
+ } finally {
984
+ this._silentConfig = false;
985
+ }
986
+ }
987
+ } finally {
988
+ configSpinner.stop(customizeModules.length > 0 ? 'Module defaults applied' : 'Module configuration complete');
989
+ }
990
+ }
991
+
992
+ // Run customized modules individually (may show interactive prompts)
993
+ for (const moduleName of customizeModules) {
994
+ await this.collectModuleConfig(moduleName, projectDir);
995
+ }
996
+
997
+ if (customizeModules.length > 0) {
998
+ await prompts.log.step('Module configuration complete');
999
+ }
1000
+ }
1001
+
1002
+ // Add metadata
1003
+ this.collectedConfig._meta = {
1004
+ version: require(path.join(getProjectRoot(), 'package.json')).version,
1005
+ installDate: new Date().toISOString(),
1006
+ lastModified: new Date().toISOString(),
1007
+ };
1008
+
1009
+ return this.collectedConfig;
1010
+ }
1011
+
1012
+ /**
1013
+ * Collect configuration for a single module (Quick Update mode - only new fields)
1014
+ * @param {string} moduleName - Module name
1015
+ * @param {string} projectDir - Target project directory
1016
+ * @param {boolean} silentMode - If true, only prompt for new/missing fields
1017
+ * @returns {boolean} True if new fields were prompted, false if all fields existed
1018
+ */
1019
+ async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
1020
+ this.currentProjectDir = projectDir;
1021
+
1022
+ // Load existing config if not already loaded
1023
+ if (!this._existingConfig) {
1024
+ await this.loadExistingConfig(projectDir);
1025
+ }
1026
+
1027
+ // Initialize allAnswers if not already initialized
1028
+ if (!this.allAnswers) {
1029
+ this.allAnswers = {};
1030
+ }
1031
+
1032
+ // Load module's config schema from module.yaml
1033
+ // First, try the standard src/modules location
1034
+ let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
1035
+
1036
+ // If not found in src/modules, we need to find it by searching the project
1037
+ if (!(await fs.pathExists(moduleConfigPath))) {
1038
+ const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
1039
+
1040
+ if (moduleSourcePath) {
1041
+ moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
1042
+ }
1043
+ }
1044
+
1045
+ let configPath = null;
1046
+ let isCustomModule = false;
1047
+
1048
+ if (await fs.pathExists(moduleConfigPath)) {
1049
+ configPath = moduleConfigPath;
1050
+ } else {
1051
+ // Check if this is a custom module with custom.yaml
1052
+ const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
1053
+
1054
+ if (moduleSourcePath) {
1055
+ const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
1056
+
1057
+ if (await fs.pathExists(rootCustomConfigPath)) {
1058
+ isCustomModule = true;
1059
+ // For custom modules, we don't have an install-config schema, so just use existing values
1060
+ // The custom.yaml values will be loaded and merged during installation
1061
+ }
1062
+ }
1063
+
1064
+ // No config schema for this module - use existing values
1065
+ if (this._existingConfig && this._existingConfig[moduleName]) {
1066
+ if (!this.collectedConfig[moduleName]) {
1067
+ this.collectedConfig[moduleName] = {};
1068
+ }
1069
+ this.collectedConfig[moduleName] = { ...this._existingConfig[moduleName] };
1070
+ }
1071
+ return false;
1072
+ }
1073
+
1074
+ const configContent = await fs.readFile(configPath, 'utf8');
1075
+ const moduleConfig = yaml.parse(configContent);
1076
+
1077
+ if (!moduleConfig) {
1078
+ return false;
1079
+ }
1080
+
1081
+ // Compare schema with existing config to find new/missing fields
1082
+ const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
1083
+ const existingKeys = this._existingConfig && this._existingConfig[moduleName] ? Object.keys(this._existingConfig[moduleName]) : [];
1084
+
1085
+ // Check if this module has no configuration keys at all (like CIS)
1086
+ // Filter out metadata fields and only count actual config objects
1087
+ const metadataFields = new Set(['code', 'name', 'header', 'subheader', 'default_selected']);
1088
+ const actualConfigKeys = configKeys.filter((key) => !metadataFields.has(key));
1089
+ const hasNoConfig = actualConfigKeys.length === 0;
1090
+
1091
+ // If module has no config keys at all, handle it specially
1092
+ if (hasNoConfig && moduleConfig.subheader) {
1093
+ const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
1094
+ await prompts.log.step(moduleDisplayName);
1095
+ await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
1096
+ return false; // No new fields
1097
+ }
1098
+
1099
+ // Find new interactive fields (with prompt)
1100
+ const newKeys = configKeys.filter((key) => {
1101
+ const item = moduleConfig[key];
1102
+ // Check if it's a config item and doesn't exist in existing config
1103
+ return item && typeof item === 'object' && item.prompt && !existingKeys.includes(key);
1104
+ });
1105
+
1106
+ // Find new static fields (without prompt, just result)
1107
+ const newStaticKeys = configKeys.filter((key) => {
1108
+ const item = moduleConfig[key];
1109
+ return item && typeof item === 'object' && !item.prompt && item.result && !existingKeys.includes(key);
1110
+ });
1111
+
1112
+ // If in silent mode and no new keys (neither interactive nor static), use existing config and skip prompts
1113
+ if (silentMode && newKeys.length === 0 && newStaticKeys.length === 0) {
1114
+ if (this._existingConfig && this._existingConfig[moduleName]) {
1115
+ if (!this.collectedConfig[moduleName]) {
1116
+ this.collectedConfig[moduleName] = {};
1117
+ }
1118
+ this.collectedConfig[moduleName] = { ...this._existingConfig[moduleName] };
1119
+
1120
+ // Special handling for user_name: ensure it has a value
1121
+ if (
1122
+ moduleName === 'core' &&
1123
+ (!this.collectedConfig[moduleName].user_name || this.collectedConfig[moduleName].user_name === '[USER_NAME]')
1124
+ ) {
1125
+ this.collectedConfig[moduleName].user_name = this.getDefaultUsername();
1126
+ }
1127
+
1128
+ // Also populate allAnswers for cross-referencing
1129
+ for (const [key, value] of Object.entries(this._existingConfig[moduleName])) {
1130
+ // Ensure user_name is properly set in allAnswers too
1131
+ let finalValue = value;
1132
+ if (moduleName === 'core' && key === 'user_name' && (!value || value === '[USER_NAME]')) {
1133
+ finalValue = this.getDefaultUsername();
1134
+ }
1135
+ this.allAnswers[`${moduleName}_${key}`] = finalValue;
1136
+ }
1137
+ } else if (moduleName === 'core') {
1138
+ // No existing core config - ensure we at least have user_name
1139
+ if (!this.collectedConfig[moduleName]) {
1140
+ this.collectedConfig[moduleName] = {};
1141
+ }
1142
+ if (!this.collectedConfig[moduleName].user_name) {
1143
+ this.collectedConfig[moduleName].user_name = this.getDefaultUsername();
1144
+ this.allAnswers[`${moduleName}_user_name`] = this.getDefaultUsername();
1145
+ }
1146
+ }
1147
+
1148
+ // Show "no config" message for modules with no new questions (that have config keys)
1149
+ await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module already up to date`);
1150
+ return false; // No new fields
1151
+ }
1152
+
1153
+ // If we have new fields (interactive or static), process them
1154
+ if (newKeys.length > 0 || newStaticKeys.length > 0) {
1155
+ const questions = [];
1156
+ const staticAnswers = {};
1157
+
1158
+ // Build questions for interactive fields
1159
+ for (const key of newKeys) {
1160
+ const item = moduleConfig[key];
1161
+ const question = await this.buildQuestion(moduleName, key, item, moduleConfig);
1162
+ if (question) {
1163
+ questions.push(question);
1164
+ }
1165
+ }
1166
+
1167
+ // Prepare static answers (no prompt, just result)
1168
+ for (const key of newStaticKeys) {
1169
+ staticAnswers[`${moduleName}_${key}`] = undefined;
1170
+ }
1171
+
1172
+ // Collect all answers (static + prompted)
1173
+ let allAnswers = { ...staticAnswers };
1174
+
1175
+ if (questions.length > 0) {
1176
+ // Only show header if we actually have questions
1177
+ await CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
1178
+ await prompts.log.message('');
1179
+ const promptedAnswers = await prompts.prompt(questions);
1180
+
1181
+ // Merge prompted answers with static answers
1182
+ Object.assign(allAnswers, promptedAnswers);
1183
+ } else if (newStaticKeys.length > 0) {
1184
+ // Only static fields, no questions - show no config message
1185
+ await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configuration updated`);
1186
+ }
1187
+
1188
+ // Store all answers for cross-referencing
1189
+ Object.assign(this.allAnswers, allAnswers);
1190
+
1191
+ // Process all answers (both static and prompted)
1192
+ // First, copy existing config to preserve values that aren't being updated
1193
+ if (this._existingConfig && this._existingConfig[moduleName]) {
1194
+ this.collectedConfig[moduleName] = { ...this._existingConfig[moduleName] };
1195
+ } else {
1196
+ this.collectedConfig[moduleName] = {};
1197
+ }
1198
+
1199
+ for (const key of Object.keys(allAnswers)) {
1200
+ const originalKey = key.replace(`${moduleName}_`, '');
1201
+ const item = moduleConfig[originalKey];
1202
+ const value = allAnswers[key];
1203
+
1204
+ let result;
1205
+ if (Array.isArray(value)) {
1206
+ result = value;
1207
+ } else if (item.result) {
1208
+ result = this.processResultTemplate(item.result, value);
1209
+ } else {
1210
+ result = value;
1211
+ }
1212
+
1213
+ // Update the collected config with new/updated values
1214
+ this.collectedConfig[moduleName][originalKey] = result;
1215
+ }
1216
+ }
1217
+
1218
+ // Copy over existing values for fields that weren't prompted
1219
+ if (this._existingConfig && this._existingConfig[moduleName]) {
1220
+ if (!this.collectedConfig[moduleName]) {
1221
+ this.collectedConfig[moduleName] = {};
1222
+ }
1223
+ for (const [key, value] of Object.entries(this._existingConfig[moduleName])) {
1224
+ if (!this.collectedConfig[moduleName][key]) {
1225
+ this.collectedConfig[moduleName][key] = value;
1226
+ this.allAnswers[`${moduleName}_${key}`] = value;
1227
+ }
1228
+ }
1229
+ }
1230
+
1231
+ await this.displayModulePostConfigNotes(moduleName, moduleConfig);
1232
+
1233
+ return newKeys.length > 0 || newStaticKeys.length > 0; // Return true if we had any new fields (interactive or static)
1234
+ }
1235
+
1236
+ /**
1237
+ * Process a result template with value substitution
1238
+ * @param {*} resultTemplate - The result template
1239
+ * @param {*} value - The value to substitute
1240
+ * @returns {*} Processed result
1241
+ */
1242
+ processResultTemplate(resultTemplate, value) {
1243
+ let result = resultTemplate;
1244
+
1245
+ if (typeof result === 'string' && value !== undefined) {
1246
+ if (typeof value === 'string') {
1247
+ result = result.replace('{value}', value);
1248
+ } else if (typeof value === 'boolean' || typeof value === 'number') {
1249
+ if (result === '{value}') {
1250
+ result = value;
1251
+ } else {
1252
+ result = result.replace('{value}', value);
1253
+ }
1254
+ } else {
1255
+ result = value;
1256
+ }
1257
+
1258
+ if (typeof result === 'string') {
1259
+ result = result.replaceAll(/{([^}]+)}/g, (match, configKey) => {
1260
+ if (configKey === 'project-root') {
1261
+ return '{project-root}';
1262
+ }
1263
+ if (configKey === 'value') {
1264
+ return match;
1265
+ }
1266
+
1267
+ let configValue = this.allAnswers[configKey] || this.allAnswers[`${configKey}`];
1268
+ if (!configValue) {
1269
+ for (const [answerKey, answerValue] of Object.entries(this.allAnswers)) {
1270
+ if (answerKey.endsWith(`_${configKey}`)) {
1271
+ configValue = answerValue;
1272
+ break;
1273
+ }
1274
+ }
1275
+ }
1276
+
1277
+ if (!configValue) {
1278
+ for (const mod of Object.keys(this.collectedConfig)) {
1279
+ if (mod !== '_meta' && this.collectedConfig[mod] && this.collectedConfig[mod][configKey]) {
1280
+ configValue = this.collectedConfig[mod][configKey];
1281
+ if (typeof configValue === 'string' && configValue.includes('{project-root}/')) {
1282
+ configValue = configValue.replace('{project-root}/', '');
1283
+ }
1284
+ break;
1285
+ }
1286
+ }
1287
+ }
1288
+
1289
+ return configValue || match;
1290
+ });
1291
+ }
1292
+ }
1293
+
1294
+ return result;
1295
+ }
1296
+
1297
+ /**
1298
+ * Get the default username from the system
1299
+ * @returns {string} Capitalized username\
1300
+ */
1301
+ getDefaultUsername() {
1302
+ let result = 'SCM';
1303
+ try {
1304
+ const os = require('node:os');
1305
+ const userInfo = os.userInfo();
1306
+ if (userInfo && userInfo.username) {
1307
+ const username = userInfo.username;
1308
+ result = username.charAt(0).toUpperCase() + username.slice(1);
1309
+ }
1310
+ } catch {
1311
+ // Do nothing, just return 'SCM'
1312
+ }
1313
+ return result;
1314
+ }
1315
+
1316
+ /**
1317
+ * Collect configuration for a single module
1318
+ * @param {string} moduleName - Module name
1319
+ * @param {string} projectDir - Target project directory
1320
+ * @param {boolean} skipLoadExisting - Skip loading existing config (for early core collection)
1321
+ * @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
1322
+ */
1323
+ async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
1324
+ this.currentProjectDir = projectDir;
1325
+ // Load existing config if needed and not already loaded
1326
+ if (!skipLoadExisting && !this._existingConfig) {
1327
+ await this.loadExistingConfig(projectDir);
1328
+ }
1329
+
1330
+ // Initialize allAnswers if not already initialized
1331
+ if (!this.allAnswers) {
1332
+ this.allAnswers = {};
1333
+ }
1334
+ // Load module's config
1335
+ // First, check if we have a custom module path for this module
1336
+ let moduleConfigPath = null;
1337
+
1338
+ if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
1339
+ const customPath = this.customModulePaths.get(moduleName);
1340
+ moduleConfigPath = path.join(customPath, 'module.yaml');
1341
+ } else {
1342
+ // Try the standard src/modules location
1343
+ moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
1344
+ }
1345
+
1346
+ // If not found in src/modules or custom paths, search the project
1347
+ if (!(await fs.pathExists(moduleConfigPath))) {
1348
+ const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
1349
+
1350
+ if (moduleSourcePath) {
1351
+ moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
1352
+ }
1353
+ }
1354
+
1355
+ let configPath = null;
1356
+ if (await fs.pathExists(moduleConfigPath)) {
1357
+ configPath = moduleConfigPath;
1358
+ } else {
1359
+ // No config for this module
1360
+ return;
1361
+ }
1362
+
1363
+ const configContent = await fs.readFile(configPath, 'utf8');
1364
+ const moduleConfig = yaml.parse(configContent);
1365
+
1366
+ if (!moduleConfig) {
1367
+ return;
1368
+ }
1369
+
1370
+ // Process each config item
1371
+ const questions = [];
1372
+ const staticAnswers = {};
1373
+ const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
1374
+
1375
+ for (const key of configKeys) {
1376
+ const item = moduleConfig[key];
1377
+
1378
+ // Skip if not a config object
1379
+ if (!item || typeof item !== 'object') {
1380
+ continue;
1381
+ }
1382
+
1383
+ // Handle static values (no prompt, just result)
1384
+ if (!item.prompt && item.result) {
1385
+ // Add to static answers with a marker value
1386
+ staticAnswers[`${moduleName}_${key}`] = undefined;
1387
+ continue;
1388
+ }
1389
+
1390
+ // Handle interactive values (with prompt)
1391
+ if (item.prompt) {
1392
+ const question = await this.buildQuestion(moduleName, key, item, moduleConfig);
1393
+ if (question) {
1394
+ questions.push(question);
1395
+ }
1396
+ }
1397
+ }
1398
+
1399
+ // Collect all answers (static + prompted)
1400
+ let allAnswers = { ...staticAnswers };
1401
+
1402
+ // If there are questions to ask, prompt for accepting defaults vs customizing
1403
+ if (questions.length > 0) {
1404
+ const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
1405
+
1406
+ // Skip prompts mode: use all defaults without asking
1407
+ if (this.skipPrompts) {
1408
+ await prompts.log.info(`Using default configuration for ${moduleDisplayName}`);
1409
+ // Use defaults for all questions
1410
+ for (const question of questions) {
1411
+ const hasDefault = question.default !== undefined && question.default !== null && question.default !== '';
1412
+ if (hasDefault && typeof question.default !== 'function') {
1413
+ allAnswers[question.name] = question.default;
1414
+ }
1415
+ }
1416
+ } else {
1417
+ if (!this._silentConfig) await prompts.log.step(`Configuring ${moduleDisplayName}`);
1418
+ let useDefaults = true;
1419
+ if (moduleName === 'core') {
1420
+ useDefaults = false; // Core: always show all questions
1421
+ } else if (this.modulesToCustomize === undefined) {
1422
+ // Fallback: original per-module confirm (backward compat for direct calls)
1423
+ const customizeAnswer = await prompts.prompt([
1424
+ {
1425
+ type: 'confirm',
1426
+ name: 'customize',
1427
+ message: 'Accept Defaults (no to customize)?',
1428
+ default: true,
1429
+ },
1430
+ ]);
1431
+ useDefaults = customizeAnswer.customize;
1432
+ } else {
1433
+ // Batch mode: use defaults unless module was selected for customization
1434
+ useDefaults = !this.modulesToCustomize.has(moduleName);
1435
+ }
1436
+
1437
+ if (useDefaults && moduleName !== 'core') {
1438
+ // Accept defaults - only ask questions that have NO default value
1439
+ const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
1440
+
1441
+ if (questionsWithoutDefaults.length > 0) {
1442
+ await prompts.log.message(` Asking required questions for ${moduleName.toUpperCase()}...`);
1443
+ const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
1444
+ Object.assign(allAnswers, promptedAnswers);
1445
+ }
1446
+
1447
+ // For questions with defaults that weren't asked, we need to process them with their default values
1448
+ const questionsWithDefaults = questions.filter((q) => q.default !== undefined && q.default !== null && q.default !== '');
1449
+ for (const question of questionsWithDefaults) {
1450
+ // Skip function defaults - these are dynamic and will be evaluated later
1451
+ if (typeof question.default === 'function') {
1452
+ continue;
1453
+ }
1454
+ allAnswers[question.name] = question.default;
1455
+ }
1456
+ } else {
1457
+ const promptedAnswers = await prompts.prompt(questions);
1458
+ Object.assign(allAnswers, promptedAnswers);
1459
+ }
1460
+ }
1461
+ }
1462
+
1463
+ // Store all answers for cross-referencing
1464
+ Object.assign(this.allAnswers, allAnswers);
1465
+
1466
+ // Process all answers (both static and prompted)
1467
+ // Always process if we have any answers or static answers
1468
+ if (Object.keys(allAnswers).length > 0 || Object.keys(staticAnswers).length > 0) {
1469
+ const answers = allAnswers;
1470
+
1471
+ // Process answers and build result values
1472
+ for (const key of Object.keys(answers)) {
1473
+ const originalKey = key.replace(`${moduleName}_`, '');
1474
+ const item = moduleConfig[originalKey];
1475
+ const value = answers[key];
1476
+
1477
+ // Build the result using the template
1478
+ let result;
1479
+
1480
+ // For arrays (multi-select), handle differently
1481
+ if (Array.isArray(value)) {
1482
+ result = value;
1483
+ } else if (item.result) {
1484
+ result = item.result;
1485
+
1486
+ // Replace placeholders only for strings
1487
+ if (typeof result === 'string' && value !== undefined) {
1488
+ // Replace {value} with the actual value
1489
+ if (typeof value === 'string') {
1490
+ result = result.replace('{value}', value);
1491
+ } else if (typeof value === 'boolean' || typeof value === 'number') {
1492
+ // For boolean and number values, if result is just "{value}", use the raw value
1493
+ if (result === '{value}') {
1494
+ result = value;
1495
+ } else {
1496
+ result = result.replace('{value}', value);
1497
+ }
1498
+ } else {
1499
+ result = value;
1500
+ }
1501
+
1502
+ // Only do further replacements if result is still a string
1503
+ if (typeof result === 'string') {
1504
+ // Replace references to other config values
1505
+ result = result.replaceAll(/{([^}]+)}/g, (match, configKey) => {
1506
+ // Check if it's a special placeholder
1507
+ if (configKey === 'project-root') {
1508
+ return '{project-root}';
1509
+ }
1510
+
1511
+ // Skip if it's the 'value' placeholder we already handled
1512
+ if (configKey === 'value') {
1513
+ return match;
1514
+ }
1515
+
1516
+ // Look for the config value across all modules
1517
+ // First check if it's in the current module's answers
1518
+ let configValue = answers[`${moduleName}_${configKey}`];
1519
+
1520
+ // Then check all answers (for cross-module references like outputFolder)
1521
+ if (!configValue) {
1522
+ // Try with various module prefixes
1523
+ for (const [answerKey, answerValue] of Object.entries(this.allAnswers)) {
1524
+ if (answerKey.endsWith(`_${configKey}`)) {
1525
+ configValue = answerValue;
1526
+ break;
1527
+ }
1528
+ }
1529
+ }
1530
+
1531
+ // Check in already collected config
1532
+ if (!configValue) {
1533
+ for (const mod of Object.keys(this.collectedConfig)) {
1534
+ if (mod !== '_meta' && this.collectedConfig[mod] && this.collectedConfig[mod][configKey]) {
1535
+ configValue = this.collectedConfig[mod][configKey];
1536
+ break;
1537
+ }
1538
+ }
1539
+ }
1540
+
1541
+ return configValue || match;
1542
+ });
1543
+ }
1544
+ }
1545
+ } else {
1546
+ result = value;
1547
+ }
1548
+
1549
+ // Store only the result value (no prompts, defaults, examples, etc.)
1550
+ if (!this.collectedConfig[moduleName]) {
1551
+ this.collectedConfig[moduleName] = {};
1552
+ }
1553
+ this.collectedConfig[moduleName][originalKey] = result;
1554
+ }
1555
+
1556
+ // No longer display completion boxes - keep output clean
1557
+ } else {
1558
+ // No questions for this module - show completion message with header if available
1559
+ const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
1560
+
1561
+ // Check if this module has NO configuration keys at all (like CIS)
1562
+ // Filter out metadata fields and only count actual config objects
1563
+ const metadataFields = new Set(['code', 'name', 'header', 'subheader', 'default_selected']);
1564
+ const actualConfigKeys = configKeys.filter((key) => !metadataFields.has(key));
1565
+ const hasNoConfig = actualConfigKeys.length === 0;
1566
+
1567
+ if (!this._silentConfig) {
1568
+ if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
1569
+ await prompts.log.step(moduleDisplayName);
1570
+ if (moduleConfig.subheader) {
1571
+ await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
1572
+ } else {
1573
+ await prompts.log.message(` \u2713 No custom configuration required`);
1574
+ }
1575
+ } else {
1576
+ // Module has config but just no questions to ask
1577
+ await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
1578
+ }
1579
+ }
1580
+ }
1581
+
1582
+ // If we have no collected config for this module, but we have a module schema,
1583
+ // ensure we have at least an empty object
1584
+ if (!this.collectedConfig[moduleName]) {
1585
+ this.collectedConfig[moduleName] = {};
1586
+
1587
+ // If we accepted defaults and have no answers, we still need to check
1588
+ // if there are any static values in the schema that should be applied
1589
+ if (moduleConfig) {
1590
+ for (const key of Object.keys(moduleConfig)) {
1591
+ if (key !== 'prompt' && moduleConfig[key] && typeof moduleConfig[key] === 'object') {
1592
+ const item = moduleConfig[key];
1593
+ // For static items (no prompt, just result), apply the result
1594
+ if (!item.prompt && item.result) {
1595
+ // Apply any placeholder replacements to the result
1596
+ let result = item.result;
1597
+ if (typeof result === 'string') {
1598
+ result = this.replacePlaceholders(result, moduleName, moduleConfig);
1599
+ }
1600
+ this.collectedConfig[moduleName][key] = result;
1601
+ }
1602
+ }
1603
+ }
1604
+ }
1605
+ }
1606
+
1607
+ await this.displayModulePostConfigNotes(moduleName, moduleConfig);
1608
+ }
1609
+
1610
+ /**
1611
+ * Replace placeholders in a string with collected config values
1612
+ * @param {string} str - String with placeholders
1613
+ * @param {string} currentModule - Current module name (to look up defaults in same module)
1614
+ * @param {Object} moduleConfig - Current module's config schema (to look up defaults)
1615
+ * @returns {string} String with placeholders replaced
1616
+ */
1617
+ replacePlaceholders(str, currentModule = null, moduleConfig = null) {
1618
+ if (typeof str !== 'string') {
1619
+ return str;
1620
+ }
1621
+
1622
+ return str.replaceAll(/{([^}]+)}/g, (match, configKey) => {
1623
+ // Preserve special placeholders
1624
+ if (configKey === 'project-root' || configKey === 'value' || configKey === 'directory_name') {
1625
+ return match;
1626
+ }
1627
+
1628
+ const configValue = this.resolveConfigValue(configKey, currentModule, moduleConfig);
1629
+
1630
+ return configValue || match;
1631
+ });
1632
+ }
1633
+
1634
+ /**
1635
+ * Clean a stored path-like value for prompt display/input reuse.
1636
+ * @param {*} value - Stored value
1637
+ * @returns {*} Cleaned value
1638
+ */
1639
+ cleanPromptValue(value) {
1640
+ if (typeof value === 'string' && value.startsWith('{project-root}/')) {
1641
+ return value.replace('{project-root}/', '');
1642
+ }
1643
+
1644
+ return value;
1645
+ }
1646
+
1647
+ /**
1648
+ * Resolve a config key from answers, collected config, existing config, or schema defaults.
1649
+ * @param {string} configKey - Config key to resolve
1650
+ * @param {string} currentModule - Current module name
1651
+ * @param {Object} moduleConfig - Current module config schema
1652
+ * @returns {*} Resolved value
1653
+ */
1654
+ resolveConfigValue(configKey, currentModule = null, moduleConfig = null) {
1655
+ // Look for the config value in allAnswers (already answered questions)
1656
+ let configValue = this.allAnswers?.[configKey] || this.allAnswers?.[`core_${configKey}`];
1657
+
1658
+ if (!configValue && this.allAnswers) {
1659
+ for (const [answerKey, answerValue] of Object.entries(this.allAnswers)) {
1660
+ if (answerKey.endsWith(`_${configKey}`)) {
1661
+ configValue = answerValue;
1662
+ break;
1663
+ }
1664
+ }
1665
+ }
1666
+
1667
+ // Prefer the current module's persisted value when re-prompting an existing install
1668
+ if (!configValue && currentModule && this._existingConfig?.[currentModule]?.[configKey] !== undefined) {
1669
+ configValue = this._existingConfig[currentModule][configKey];
1670
+ }
1671
+
1672
+ // Check in already collected config
1673
+ if (!configValue) {
1674
+ for (const mod of Object.keys(this.collectedConfig)) {
1675
+ if (mod !== '_meta' && this.collectedConfig[mod] && this.collectedConfig[mod][configKey]) {
1676
+ configValue = this.collectedConfig[mod][configKey];
1677
+ break;
1678
+ }
1679
+ }
1680
+ }
1681
+
1682
+ // Fall back to other existing module config values
1683
+ if (!configValue && this._existingConfig) {
1684
+ for (const mod of Object.keys(this._existingConfig)) {
1685
+ if (mod !== '_meta' && this._existingConfig[mod] && this._existingConfig[mod][configKey]) {
1686
+ configValue = this._existingConfig[mod][configKey];
1687
+ break;
1688
+ }
1689
+ }
1690
+ }
1691
+
1692
+ // If still not found and we're in the same module, use the default from the config schema
1693
+ if (!configValue && currentModule && moduleConfig && moduleConfig[configKey]) {
1694
+ const referencedItem = moduleConfig[configKey];
1695
+ if (referencedItem && referencedItem.default !== undefined) {
1696
+ configValue = referencedItem.default;
1697
+ }
1698
+ }
1699
+
1700
+ return this.cleanPromptValue(configValue);
1701
+ }
1702
+
1703
+ /**
1704
+ * Convert an existing stored value back into the prompt-facing value for templated fields.
1705
+ * For example, "{test_artifacts}/{value}" + "_scm-output/test-artifacts/test-design"
1706
+ * becomes "test-design" so the template is not applied twice on modify.
1707
+ * @param {*} existingValue - Stored config value
1708
+ * @param {string} moduleName - Module name
1709
+ * @param {Object} item - Config item definition
1710
+ * @param {Object} moduleConfig - Current module config schema
1711
+ * @returns {*} Prompt-facing default value
1712
+ */
1713
+ normalizeExistingValueForPrompt(existingValue, moduleName, item, moduleConfig = null) {
1714
+ const cleanedValue = this.cleanPromptValue(existingValue);
1715
+
1716
+ if (typeof cleanedValue !== 'string' || typeof item?.result !== 'string' || !item.result.includes('{value}')) {
1717
+ return cleanedValue;
1718
+ }
1719
+
1720
+ const [prefixTemplate = '', suffixTemplate = ''] = item.result.split('{value}');
1721
+ const prefix = this.cleanPromptValue(this.replacePlaceholders(prefixTemplate, moduleName, moduleConfig));
1722
+ const suffix = this.cleanPromptValue(this.replacePlaceholders(suffixTemplate, moduleName, moduleConfig));
1723
+
1724
+ if ((prefix && !cleanedValue.startsWith(prefix)) || (suffix && !cleanedValue.endsWith(suffix))) {
1725
+ return cleanedValue;
1726
+ }
1727
+
1728
+ const startIndex = prefix.length;
1729
+ const endIndex = suffix ? cleanedValue.length - suffix.length : cleanedValue.length;
1730
+ if (endIndex < startIndex) {
1731
+ return cleanedValue;
1732
+ }
1733
+
1734
+ let promptValue = cleanedValue.slice(startIndex, endIndex);
1735
+ if (promptValue.startsWith('/')) {
1736
+ promptValue = promptValue.slice(1);
1737
+ }
1738
+ if (promptValue.endsWith('/')) {
1739
+ promptValue = promptValue.slice(0, -1);
1740
+ }
1741
+
1742
+ return promptValue || cleanedValue;
1743
+ }
1744
+
1745
+ /**
1746
+ * Build a prompt question from a config item
1747
+ * @param {string} moduleName - Module name
1748
+ * @param {string} key - Config key
1749
+ * @param {Object} item - Config item definition
1750
+ * @param {Object} moduleConfig - Full module config schema (for resolving defaults)
1751
+ */
1752
+ async buildQuestion(moduleName, key, item, moduleConfig = null) {
1753
+ const questionName = `${moduleName}_${key}`;
1754
+
1755
+ // Check for existing value
1756
+ let existingValue = null;
1757
+ if (this._existingConfig && this._existingConfig[moduleName]) {
1758
+ existingValue = this._existingConfig[moduleName][key];
1759
+ existingValue = this.normalizeExistingValueForPrompt(existingValue, moduleName, item, moduleConfig);
1760
+ }
1761
+
1762
+ // Special handling for user_name: default to system user
1763
+ if (moduleName === 'core' && key === 'user_name' && !existingValue) {
1764
+ item.default = this.getDefaultUsername();
1765
+ }
1766
+
1767
+ // Determine question type and default value
1768
+ let questionType = 'input';
1769
+ let defaultValue = item.default;
1770
+ let choices = null;
1771
+
1772
+ // Check if default contains references to other fields in the same module
1773
+ const hasSameModuleReference = typeof defaultValue === 'string' && defaultValue.match(/{([^}]+)}/);
1774
+ let dynamicDefault = false;
1775
+
1776
+ // Replace placeholders in default value with collected config values
1777
+ if (typeof defaultValue === 'string') {
1778
+ if (defaultValue.includes('{directory_name}') && this.currentProjectDir) {
1779
+ const dirName = path.basename(this.currentProjectDir);
1780
+ defaultValue = defaultValue.replaceAll('{directory_name}', dirName);
1781
+ }
1782
+
1783
+ // Check if this references another field in the same module (for dynamic defaults)
1784
+ if (hasSameModuleReference && moduleConfig) {
1785
+ const matches = defaultValue.match(/{([^}]+)}/g);
1786
+ if (matches) {
1787
+ for (const match of matches) {
1788
+ const fieldName = match.slice(1, -1); // Remove { }
1789
+ // Check if this field exists in the same module config
1790
+ if (moduleConfig[fieldName]) {
1791
+ dynamicDefault = true;
1792
+ break;
1793
+ }
1794
+ }
1795
+ }
1796
+ }
1797
+
1798
+ // If not dynamic, replace placeholders now
1799
+ if (!dynamicDefault) {
1800
+ defaultValue = this.replacePlaceholders(defaultValue, moduleName, moduleConfig);
1801
+ }
1802
+
1803
+ // Strip {project-root}/ from defaults since it will be added back by result template
1804
+ // This makes the display cleaner and user input simpler
1805
+ if (defaultValue.includes('{project-root}/')) {
1806
+ defaultValue = defaultValue.replace('{project-root}/', '');
1807
+ }
1808
+ }
1809
+
1810
+ // Handle different question types
1811
+ if (item['single-select']) {
1812
+ questionType = 'list';
1813
+ choices = item['single-select'].map((choice) => {
1814
+ // If choice is an object with label and value
1815
+ if (typeof choice === 'object' && choice.label && choice.value !== undefined) {
1816
+ return {
1817
+ name: choice.label,
1818
+ value: choice.value,
1819
+ };
1820
+ }
1821
+ // Otherwise it's a simple string choice
1822
+ return {
1823
+ name: choice,
1824
+ value: choice,
1825
+ };
1826
+ });
1827
+ if (existingValue) {
1828
+ defaultValue = existingValue;
1829
+ }
1830
+ } else if (item['multi-select']) {
1831
+ questionType = 'checkbox';
1832
+ choices = item['multi-select'].map((choice) => {
1833
+ // If choice is an object with label and value
1834
+ if (typeof choice === 'object' && choice.label && choice.value !== undefined) {
1835
+ return {
1836
+ name: choice.label,
1837
+ value: choice.value,
1838
+ checked: existingValue
1839
+ ? existingValue.includes(choice.value)
1840
+ : item.default && Array.isArray(item.default)
1841
+ ? item.default.includes(choice.value)
1842
+ : false,
1843
+ };
1844
+ }
1845
+ // Otherwise it's a simple string choice
1846
+ return {
1847
+ name: choice,
1848
+ value: choice,
1849
+ checked: existingValue
1850
+ ? existingValue.includes(choice)
1851
+ : item.default && Array.isArray(item.default)
1852
+ ? item.default.includes(choice)
1853
+ : false,
1854
+ };
1855
+ });
1856
+ } else if (typeof defaultValue === 'boolean') {
1857
+ questionType = 'confirm';
1858
+ }
1859
+
1860
+ // Build the prompt message
1861
+ let message = '';
1862
+
1863
+ // Handle array prompts for multi-line messages
1864
+ if (Array.isArray(item.prompt)) {
1865
+ message = item.prompt.join('\n');
1866
+ } else {
1867
+ message = item.prompt;
1868
+ }
1869
+
1870
+ // Replace placeholders in prompt message with collected config values
1871
+ if (typeof message === 'string') {
1872
+ message = this.replacePlaceholders(message, moduleName, moduleConfig);
1873
+ }
1874
+
1875
+ // Add current value indicator for existing configs
1876
+ const color = await prompts.getColor();
1877
+ if (existingValue !== null && existingValue !== undefined) {
1878
+ if (typeof existingValue === 'boolean') {
1879
+ message += color.dim(` (current: ${existingValue ? 'true' : 'false'})`);
1880
+ } else if (Array.isArray(existingValue)) {
1881
+ message += color.dim(` (current: ${existingValue.join(', ')})`);
1882
+ } else if (questionType !== 'list') {
1883
+ // Show the cleaned value (without {project-root}/) for display
1884
+ message += color.dim(` (current: ${existingValue})`);
1885
+ }
1886
+ } else if (item.example && questionType === 'input') {
1887
+ // Show example for input fields
1888
+ let exampleText = typeof item.example === 'string' ? item.example : JSON.stringify(item.example);
1889
+ // Replace placeholders in example
1890
+ if (typeof exampleText === 'string') {
1891
+ exampleText = this.replacePlaceholders(exampleText, moduleName, moduleConfig);
1892
+ exampleText = exampleText.replace('{project-root}/', '');
1893
+ }
1894
+ message += color.dim(` (e.g., ${exampleText})`);
1895
+ }
1896
+
1897
+ // Build the question object
1898
+ const question = {
1899
+ type: questionType,
1900
+ name: questionName,
1901
+ message: message,
1902
+ };
1903
+
1904
+ // Set default - if it's dynamic, use a function that the prompt will evaluate with current answers
1905
+ // But if we have an existing value, always use that instead
1906
+ if (existingValue !== null && existingValue !== undefined && questionType !== 'list') {
1907
+ question.default = existingValue;
1908
+ } else if (dynamicDefault && typeof item.default === 'string') {
1909
+ const originalDefault = item.default;
1910
+ question.default = (answers) => {
1911
+ // Replace placeholders using answers from previous questions in the same batch
1912
+ let resolved = originalDefault;
1913
+ resolved = resolved.replaceAll(/{([^}]+)}/g, (match, fieldName) => {
1914
+ // Look for the answer in the current batch (prefixed with module name)
1915
+ const answerKey = `${moduleName}_${fieldName}`;
1916
+ if (answers[answerKey] !== undefined) {
1917
+ return answers[answerKey];
1918
+ }
1919
+ // Fall back to collected config
1920
+ return this.collectedConfig[moduleName]?.[fieldName] || match;
1921
+ });
1922
+ // Strip {project-root}/ for cleaner display
1923
+ if (resolved.includes('{project-root}/')) {
1924
+ resolved = resolved.replace('{project-root}/', '');
1925
+ }
1926
+ return resolved;
1927
+ };
1928
+ } else {
1929
+ question.default = defaultValue;
1930
+ }
1931
+
1932
+ // Add choices for select types
1933
+ if (choices) {
1934
+ question.choices = choices;
1935
+ }
1936
+
1937
+ // Add validation for input fields
1938
+ if (questionType === 'input') {
1939
+ question.validate = (input) => {
1940
+ if (!input && item.required) {
1941
+ return 'This field is required';
1942
+ }
1943
+ // Validate against regex pattern if provided
1944
+ if (input && item.regex) {
1945
+ const regex = new RegExp(item.regex);
1946
+ if (!regex.test(input)) {
1947
+ return `Invalid format. Must match pattern: ${item.regex}`;
1948
+ }
1949
+ }
1950
+ return true;
1951
+ };
1952
+ }
1953
+
1954
+ // Add validation for checkbox (multi-select) fields
1955
+ if (questionType === 'checkbox' && item.required) {
1956
+ question.validate = (answers) => {
1957
+ if (!answers || answers.length === 0) {
1958
+ return 'At least one option must be selected';
1959
+ }
1960
+ return true;
1961
+ };
1962
+ }
1963
+
1964
+ return question;
1965
+ }
1966
+
1967
+ /**
1968
+ * Display post-configuration notes for a module
1969
+ * Shows prerequisite guidance based on collected config values
1970
+ * Reads notes from the module's `post-install-notes` section in module.yaml
1971
+ * Supports two formats:
1972
+ * - Simple string: always displayed
1973
+ * - Object keyed by config field name, with value-specific messages
1974
+ * @param {string} moduleName - Module name
1975
+ * @param {Object} moduleConfig - Parsed module.yaml content
1976
+ */
1977
+ async displayModulePostConfigNotes(moduleName, moduleConfig) {
1978
+ if (this._silentConfig) return;
1979
+ if (!moduleConfig || !moduleConfig['post-install-notes']) return;
1980
+
1981
+ const notes = moduleConfig['post-install-notes'];
1982
+ const color = await prompts.getColor();
1983
+
1984
+ // Format 1: Simple string - always display
1985
+ if (typeof notes === 'string') {
1986
+ await prompts.log.message('');
1987
+ for (const line of notes.trim().split('\n')) {
1988
+ await prompts.log.message(color.dim(line));
1989
+ }
1990
+ return;
1991
+ }
1992
+
1993
+ // Format 2: Conditional on config values
1994
+ if (typeof notes === 'object') {
1995
+ const config = this.collectedConfig[moduleName];
1996
+ if (!config) return;
1997
+
1998
+ let hasOutput = false;
1999
+ for (const [configKey, valueMessages] of Object.entries(notes)) {
2000
+ const selectedValue = config[configKey];
2001
+ if (!selectedValue || !valueMessages[selectedValue]) continue;
2002
+
2003
+ if (hasOutput) await prompts.log.message('');
2004
+ hasOutput = true;
2005
+
2006
+ const message = valueMessages[selectedValue];
2007
+ for (const line of message.trim().split('\n')) {
2008
+ const trimmedLine = line.trim();
2009
+ if (trimmedLine.endsWith(':') && !trimmedLine.startsWith(' ')) {
2010
+ await prompts.log.info(color.bold(trimmedLine));
2011
+ } else {
2012
+ await prompts.log.message(color.dim(' ' + trimmedLine));
2013
+ }
2014
+ }
2015
+ }
2016
+ }
2017
+ }
2018
+
2019
+ /**
2020
+ * Deep merge two objects
2021
+ * @param {Object} target - Target object
2022
+ * @param {Object} source - Source object
2023
+ */
2024
+ deepMerge(target, source) {
2025
+ const result = { ...target };
2026
+
2027
+ for (const key in source) {
2028
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
2029
+ if (result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
2030
+ result[key] = this.deepMerge(result[key], source[key]);
2031
+ } else {
2032
+ result[key] = source[key];
2033
+ }
2034
+ } else {
2035
+ result[key] = source[key];
2036
+ }
2037
+ }
2038
+
2039
+ return result;
2040
+ }
2041
+ }
2042
+
2043
+ module.exports = { OfficialModules };