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,1790 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const { Manifest } = require('./manifest');
4
+ const { OfficialModules } = require('../modules/official-modules');
5
+ const { CustomModules } = require('../modules/custom-modules');
6
+ const { IdeManager } = require('../ide/manager');
7
+ const { FileOps } = require('../file-ops');
8
+ const { Config } = require('./config');
9
+ const { getProjectRoot, getSourcePath } = require('../project-root');
10
+ const { ManifestGenerator } = require('./manifest-generator');
11
+ const prompts = require('../prompts');
12
+ const { SCM_FOLDER_NAME } = require('../ide/shared/path-utils');
13
+ const { InstallPaths } = require('./install-paths');
14
+ const { ExternalModuleManager } = require('../modules/external-manager');
15
+
16
+ const { ExistingInstall } = require('./existing-install');
17
+
18
+ class Installer {
19
+ constructor() {
20
+ this.externalModuleManager = new ExternalModuleManager();
21
+ this.manifest = new Manifest();
22
+ this.customModules = new CustomModules();
23
+ this.ideManager = new IdeManager();
24
+ this.fileOps = new FileOps();
25
+ this.installedFiles = new Set(); // Track all installed files
26
+ this.scmFolderName = SCM_FOLDER_NAME;
27
+ }
28
+
29
+ /**
30
+ * Main installation method
31
+ * @param {Object} config - Installation configuration
32
+ * @param {string} config.directory - Target directory
33
+ * @param {string[]} config.modules - Modules to install (including 'core')
34
+ * @param {string[]} config.ides - IDEs to configure
35
+ */
36
+ async install(originalConfig) {
37
+ let updateState = null;
38
+
39
+ try {
40
+ const config = Config.build(originalConfig);
41
+ const paths = await InstallPaths.create(config);
42
+ const officialModules = await OfficialModules.build(config, paths);
43
+ const existingInstall = await ExistingInstall.detect(paths.scmDir);
44
+
45
+ await this.customModules.discoverPaths(originalConfig, paths);
46
+
47
+ if (existingInstall.installed) {
48
+ await this._removeDeselectedModules(existingInstall, config, paths);
49
+ updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
50
+ await this._removeDeselectedIdes(existingInstall, config, paths);
51
+ }
52
+
53
+ await this._validateIdeSelection(config);
54
+
55
+ // Results collector for consolidated summary
56
+ const results = [];
57
+ const addResult = (step, status, detail = '') => results.push({ step, status, detail });
58
+
59
+ await this._cacheCustomModules(paths, addResult);
60
+
61
+ // Compute module lists: official = selected minus custom, all = both
62
+ const customModuleIds = new Set(this.customModules.paths.keys());
63
+ const officialModuleIds = (config.modules || []).filter((m) => !customModuleIds.has(m));
64
+ const allModules = [...officialModuleIds, ...[...customModuleIds].filter((id) => !officialModuleIds.includes(id))];
65
+
66
+ await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules);
67
+
68
+ await this._setupIdes(config, allModules, paths, addResult);
69
+
70
+ const restoreResult = await this._restoreUserFiles(paths, updateState);
71
+
72
+ // Render consolidated summary
73
+ await this.renderInstallSummary(results, {
74
+ scmDir: paths.scmDir,
75
+ modules: config.modules,
76
+ ides: config.ides,
77
+ customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
78
+ modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
79
+ });
80
+
81
+ return {
82
+ success: true,
83
+ path: paths.scmDir,
84
+ modules: config.modules,
85
+ ides: config.ides,
86
+ projectDir: paths.projectRoot,
87
+ };
88
+ } catch (error) {
89
+ await prompts.log.error('Installation failed');
90
+
91
+ // Clean up any temp backup directories that were created before the failure
92
+ try {
93
+ if (updateState?.tempBackupDir && (await fs.pathExists(updateState.tempBackupDir))) {
94
+ await fs.remove(updateState.tempBackupDir);
95
+ }
96
+ if (updateState?.tempModifiedBackupDir && (await fs.pathExists(updateState.tempModifiedBackupDir))) {
97
+ await fs.remove(updateState.tempModifiedBackupDir);
98
+ }
99
+ } catch {
100
+ // Best-effort cleanup — don't mask the original error
101
+ }
102
+
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Remove modules that were previously installed but are no longer selected.
109
+ * No confirmation — the user's module selection is the decision.
110
+ */
111
+ async _removeDeselectedModules(existingInstall, config, paths) {
112
+ const previouslyInstalled = new Set(existingInstall.moduleIds);
113
+ const newlySelected = new Set(config.modules || []);
114
+ const toRemove = [...previouslyInstalled].filter((m) => !newlySelected.has(m) && m !== 'core');
115
+
116
+ for (const moduleId of toRemove) {
117
+ const modulePath = paths.moduleDir(moduleId);
118
+ try {
119
+ if (await fs.pathExists(modulePath)) {
120
+ await fs.remove(modulePath);
121
+ }
122
+ } catch (error) {
123
+ await prompts.log.warn(`Warning: Failed to remove ${moduleId}: ${error.message}`);
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Fail fast if all selected IDEs are suspended.
130
+ */
131
+ async _validateIdeSelection(config) {
132
+ if (!config.ides || config.ides.length === 0) return;
133
+
134
+ await this.ideManager.ensureInitialized();
135
+ const suspendedIdes = config.ides.filter((ide) => {
136
+ const handler = this.ideManager.handlers.get(ide);
137
+ return handler?.platformConfig?.suspended;
138
+ });
139
+
140
+ if (suspendedIdes.length > 0 && suspendedIdes.length === config.ides.length) {
141
+ for (const ide of suspendedIdes) {
142
+ const handler = this.ideManager.handlers.get(ide);
143
+ await prompts.log.error(`${handler.displayName || ide}: ${handler.platformConfig.suspended}`);
144
+ }
145
+ throw new Error(
146
+ `All selected tool(s) are suspended: ${suspendedIdes.join(', ')}. Installation aborted to prevent upgrading _scm/ without a working IDE configuration.`,
147
+ );
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Remove IDEs that were previously installed but are no longer selected.
153
+ * No confirmation — the user's IDE selection is the decision.
154
+ */
155
+ async _removeDeselectedIdes(existingInstall, config, paths) {
156
+ const previouslyInstalled = new Set(existingInstall.ides);
157
+ const newlySelected = new Set(config.ides || []);
158
+ const toRemove = [...previouslyInstalled].filter((ide) => !newlySelected.has(ide));
159
+
160
+ if (toRemove.length === 0) return;
161
+
162
+ await this.ideManager.ensureInitialized();
163
+ for (const ide of toRemove) {
164
+ try {
165
+ const handler = this.ideManager.handlers.get(ide);
166
+ if (handler) {
167
+ await handler.cleanup(paths.projectRoot);
168
+ }
169
+ } catch (error) {
170
+ await prompts.log.warn(`Warning: Failed to remove ${ide}: ${error.message}`);
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Cache custom modules into the local cache directory.
177
+ * Updates this.customModules.paths in place with cached locations.
178
+ */
179
+ async _cacheCustomModules(paths, addResult) {
180
+ if (!this.customModules.paths || this.customModules.paths.size === 0) return;
181
+
182
+ const { CustomModuleCache } = require('./custom-module-cache');
183
+ const customCache = new CustomModuleCache(paths.scmDir);
184
+
185
+ for (const [moduleId, sourcePath] of this.customModules.paths) {
186
+ const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
187
+ sourcePath: sourcePath,
188
+ });
189
+ this.customModules.paths.set(moduleId, cachedInfo.cachePath);
190
+ }
191
+
192
+ addResult('Custom modules cached', 'ok');
193
+ }
194
+
195
+ /**
196
+ * Install modules, create directories, generate configs and manifests.
197
+ */
198
+ async _installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules) {
199
+ const isQuickUpdate = config.isQuickUpdate();
200
+ const moduleConfigs = officialModules.moduleConfigs;
201
+
202
+ const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
203
+
204
+ const installTasks = [];
205
+
206
+ if (allModules.length > 0) {
207
+ installTasks.push({
208
+ title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
209
+ task: async (message) => {
210
+ const installedModuleNames = new Set();
211
+
212
+ await this._installOfficialModules(config, paths, officialModuleIds, addResult, isQuickUpdate, officialModules, {
213
+ message,
214
+ installedModuleNames,
215
+ });
216
+
217
+ await this._installCustomModules(config, paths, addResult, officialModules, {
218
+ message,
219
+ installedModuleNames,
220
+ });
221
+
222
+ return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
223
+ },
224
+ });
225
+ }
226
+
227
+ installTasks.push({
228
+ title: 'Creating module directories',
229
+ task: async (message) => {
230
+ const verboseMode = process.env.SCM_VERBOSE_INSTALL === 'true' || config.verbose;
231
+ const moduleLogger = {
232
+ log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined),
233
+ error: async (msg) => await prompts.log.error(msg),
234
+ warn: async (msg) => await prompts.log.warn(msg),
235
+ };
236
+
237
+ if (config.modules && config.modules.length > 0) {
238
+ for (const moduleName of config.modules) {
239
+ message(`Setting up ${moduleName}...`);
240
+ const result = await officialModules.createModuleDirectories(moduleName, paths.scmDir, {
241
+ installedIDEs: config.ides || [],
242
+ moduleConfig: moduleConfigs[moduleName] || {},
243
+ existingModuleConfig: officialModules.existingConfig?.[moduleName] || {},
244
+ coreConfig: moduleConfigs.core || {},
245
+ logger: moduleLogger,
246
+ silent: true,
247
+ });
248
+ if (result) {
249
+ dirResults.createdDirs.push(...result.createdDirs);
250
+ dirResults.movedDirs.push(...(result.movedDirs || []));
251
+ dirResults.createdWdsFolders.push(...result.createdWdsFolders);
252
+ }
253
+ }
254
+ }
255
+
256
+ addResult('Module directories', 'ok');
257
+ return 'Module directories created';
258
+ },
259
+ });
260
+
261
+ const configTask = {
262
+ title: 'Generating configurations',
263
+ task: async (message) => {
264
+ await this.generateModuleConfigs(paths.scmDir, moduleConfigs);
265
+ addResult('Configurations', 'ok', 'generated');
266
+
267
+ this.installedFiles.add(paths.manifestFile());
268
+ this.installedFiles.add(paths.agentManifest());
269
+
270
+ message('Generating manifests...');
271
+ const manifestGen = new ManifestGenerator();
272
+
273
+ const allModulesForManifest = config.isQuickUpdate()
274
+ ? originalConfig._existingModules || allModules || []
275
+ : originalConfig._preserveModules
276
+ ? [...allModules, ...originalConfig._preserveModules]
277
+ : allModules || [];
278
+
279
+ let modulesForCsvPreserve;
280
+ if (config.isQuickUpdate()) {
281
+ modulesForCsvPreserve = originalConfig._existingModules || allModules || [];
282
+ } else {
283
+ modulesForCsvPreserve = originalConfig._preserveModules ? [...allModules, ...originalConfig._preserveModules] : allModules;
284
+ }
285
+
286
+ await manifestGen.generateManifests(paths.scmDir, allModulesForManifest, [...this.installedFiles], {
287
+ ides: config.ides || [],
288
+ preservedModules: modulesForCsvPreserve,
289
+ });
290
+
291
+ message('Generating help catalog...');
292
+ await this.mergeModuleHelpCatalogs(paths.scmDir);
293
+ addResult('Help catalog', 'ok');
294
+
295
+ return 'Configurations generated';
296
+ },
297
+ };
298
+ installTasks.push(configTask);
299
+
300
+ // Run install + dirs first, then render dir output, then run config generation
301
+ const mainTasks = installTasks.filter((t) => t !== configTask);
302
+ await prompts.tasks(mainTasks);
303
+
304
+ const color = await prompts.getColor();
305
+ if (dirResults.movedDirs.length > 0) {
306
+ const lines = dirResults.movedDirs.map((d) => ` ${d}`).join('\n');
307
+ await prompts.log.message(color.cyan(`Moved directories:\n${lines}`));
308
+ }
309
+ if (dirResults.createdDirs.length > 0) {
310
+ const lines = dirResults.createdDirs.map((d) => ` ${d}`).join('\n');
311
+ await prompts.log.message(color.yellow(`Created directories:\n${lines}`));
312
+ }
313
+ if (dirResults.createdWdsFolders.length > 0) {
314
+ const lines = dirResults.createdWdsFolders.map((f) => color.dim(` \u2713 ${f}/`)).join('\n');
315
+ await prompts.log.message(color.cyan(`Created WDS folder structure:\n${lines}`));
316
+ }
317
+
318
+ await prompts.tasks([configTask]);
319
+ }
320
+
321
+ /**
322
+ * Set up IDE integrations for each selected IDE.
323
+ */
324
+ async _setupIdes(config, allModules, paths, addResult) {
325
+ if (config.skipIde || !config.ides || config.ides.length === 0) return;
326
+
327
+ await this.ideManager.ensureInitialized();
328
+ const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
329
+
330
+ if (validIdes.length === 0) {
331
+ addResult('IDE configuration', 'warn', 'no valid IDEs selected');
332
+ return;
333
+ }
334
+
335
+ for (const ide of validIdes) {
336
+ const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.scmDir, {
337
+ selectedModules: allModules || [],
338
+ verbose: config.verbose,
339
+ });
340
+
341
+ if (setupResult.success) {
342
+ addResult(ide, 'ok', setupResult.detail || '');
343
+ } else {
344
+ addResult(ide, 'error', setupResult.error || 'failed');
345
+ }
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Restore custom and modified files that were backed up before the update.
351
+ * No-op for fresh installs (updateState is null).
352
+ * @param {Object} paths - InstallPaths instance
353
+ * @param {Object|null} updateState - From _prepareUpdateState, or null for fresh installs
354
+ * @returns {Object} { customFiles, modifiedFiles } — lists of restored files
355
+ */
356
+ async _restoreUserFiles(paths, updateState) {
357
+ const noFiles = { customFiles: [], modifiedFiles: [] };
358
+
359
+ if (!updateState || (updateState.customFiles.length === 0 && updateState.modifiedFiles.length === 0)) {
360
+ return noFiles;
361
+ }
362
+
363
+ let restoredCustomFiles = [];
364
+ let restoredModifiedFiles = [];
365
+
366
+ await prompts.tasks([
367
+ {
368
+ title: 'Finalizing installation',
369
+ task: async (message) => {
370
+ if (updateState.customFiles.length > 0) {
371
+ message(`Restoring ${updateState.customFiles.length} custom files...`);
372
+
373
+ for (const originalPath of updateState.customFiles) {
374
+ const relativePath = path.relative(paths.scmDir, originalPath);
375
+ const backupPath = path.join(updateState.tempBackupDir, relativePath);
376
+
377
+ if (await fs.pathExists(backupPath)) {
378
+ await fs.ensureDir(path.dirname(originalPath));
379
+ await fs.copy(backupPath, originalPath, { overwrite: true });
380
+ }
381
+ }
382
+
383
+ if (updateState.tempBackupDir && (await fs.pathExists(updateState.tempBackupDir))) {
384
+ await fs.remove(updateState.tempBackupDir);
385
+ }
386
+
387
+ restoredCustomFiles = updateState.customFiles;
388
+ }
389
+
390
+ if (updateState.modifiedFiles.length > 0) {
391
+ restoredModifiedFiles = updateState.modifiedFiles;
392
+
393
+ if (updateState.tempModifiedBackupDir && (await fs.pathExists(updateState.tempModifiedBackupDir))) {
394
+ message(`Restoring ${restoredModifiedFiles.length} modified files as .bak...`);
395
+
396
+ for (const modifiedFile of restoredModifiedFiles) {
397
+ const relativePath = path.relative(paths.scmDir, modifiedFile.path);
398
+ const tempBackupPath = path.join(updateState.tempModifiedBackupDir, relativePath);
399
+ const bakPath = modifiedFile.path + '.bak';
400
+
401
+ if (await fs.pathExists(tempBackupPath)) {
402
+ await fs.ensureDir(path.dirname(bakPath));
403
+ await fs.copy(tempBackupPath, bakPath, { overwrite: true });
404
+ }
405
+ }
406
+
407
+ await fs.remove(updateState.tempModifiedBackupDir);
408
+ }
409
+ }
410
+
411
+ return 'Installation finalized';
412
+ },
413
+ },
414
+ ]);
415
+
416
+ return { customFiles: restoredCustomFiles, modifiedFiles: restoredModifiedFiles };
417
+ }
418
+
419
+ /**
420
+ * Scan the custom module cache directory and register any cached custom modules
421
+ * that aren't already known from the manifest or external module list.
422
+ * @param {Object} paths - InstallPaths instance
423
+ */
424
+ async _scanCachedCustomModules(paths) {
425
+ const cacheDir = paths.customCacheDir;
426
+ if (!(await fs.pathExists(cacheDir))) {
427
+ return;
428
+ }
429
+
430
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
431
+
432
+ for (const cachedModule of cachedModules) {
433
+ const moduleId = cachedModule.name;
434
+ const cachedPath = path.join(cacheDir, moduleId);
435
+
436
+ // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
437
+ if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
438
+ continue;
439
+ }
440
+
441
+ // Skip if we already have this module from manifest
442
+ if (this.customModules.paths.has(moduleId)) {
443
+ continue;
444
+ }
445
+
446
+ // Check if this is an external official module - skip cache for those
447
+ const isExternal = await this.externalModuleManager.hasModule(moduleId);
448
+ if (isExternal) {
449
+ continue;
450
+ }
451
+
452
+ // Check if this is actually a custom module (has module.yaml)
453
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
454
+ if (await fs.pathExists(moduleYamlPath)) {
455
+ this.customModules.paths.set(moduleId, cachedPath);
456
+ }
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Common update preparation: detect files, preserve core config, scan cache, back up.
462
+ * @param {Object} paths - InstallPaths instance
463
+ * @param {Object} config - Clean config (may have coreConfig updated)
464
+ * @param {Object} existingInstall - Detection result
465
+ * @param {Object} officialModules - OfficialModules instance
466
+ * @returns {Object} Update state: { customFiles, modifiedFiles, tempBackupDir, tempModifiedBackupDir }
467
+ */
468
+ async _prepareUpdateState(paths, config, existingInstall, officialModules) {
469
+ // Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
470
+ const existingFilesManifest = await this.readFilesManifest(paths.scmDir);
471
+ const { customFiles, modifiedFiles } = await this.detectCustomFiles(paths.scmDir, existingFilesManifest);
472
+
473
+ // Preserve existing core configuration during updates
474
+ // (no-op for quick-update which already has core config from collectModuleConfigQuick)
475
+ const coreConfigPath = paths.moduleConfig('core');
476
+ if ((await fs.pathExists(coreConfigPath)) && (!config.coreConfig || Object.keys(config.coreConfig).length === 0)) {
477
+ try {
478
+ const yaml = require('yaml');
479
+ const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
480
+ const existingCoreConfig = yaml.parse(coreConfigContent);
481
+
482
+ config.coreConfig = existingCoreConfig;
483
+ officialModules.moduleConfigs.core = existingCoreConfig;
484
+ } catch (error) {
485
+ await prompts.log.warn(`Warning: Could not read existing core config: ${error.message}`);
486
+ }
487
+ }
488
+
489
+ await this._scanCachedCustomModules(paths);
490
+
491
+ const backupDirs = await this._backupUserFiles(paths, customFiles, modifiedFiles);
492
+
493
+ return {
494
+ customFiles,
495
+ modifiedFiles,
496
+ tempBackupDir: backupDirs.tempBackupDir,
497
+ tempModifiedBackupDir: backupDirs.tempModifiedBackupDir,
498
+ };
499
+ }
500
+
501
+ /**
502
+ * Back up custom and modified files to temp directories before overwriting.
503
+ * Returns the temp directory paths (or undefined if no files to back up).
504
+ * @param {Object} paths - InstallPaths instance
505
+ * @param {string[]} customFiles - Absolute paths of custom (user-added) files
506
+ * @param {Object[]} modifiedFiles - Array of { path, relativePath } for modified files
507
+ * @returns {Object} { tempBackupDir, tempModifiedBackupDir } — undefined if no files
508
+ */
509
+ async _backupUserFiles(paths, customFiles, modifiedFiles) {
510
+ let tempBackupDir;
511
+ let tempModifiedBackupDir;
512
+
513
+ if (customFiles.length > 0) {
514
+ tempBackupDir = path.join(paths.projectRoot, '_scm-custom-backup-temp');
515
+ await fs.ensureDir(tempBackupDir);
516
+
517
+ for (const customFile of customFiles) {
518
+ const relativePath = path.relative(paths.scmDir, customFile);
519
+ const backupPath = path.join(tempBackupDir, relativePath);
520
+ await fs.ensureDir(path.dirname(backupPath));
521
+ await fs.copy(customFile, backupPath);
522
+ }
523
+ }
524
+
525
+ if (modifiedFiles.length > 0) {
526
+ tempModifiedBackupDir = path.join(paths.projectRoot, '_scm-modified-backup-temp');
527
+ await fs.ensureDir(tempModifiedBackupDir);
528
+
529
+ for (const modifiedFile of modifiedFiles) {
530
+ const relativePath = path.relative(paths.scmDir, modifiedFile.path);
531
+ const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
532
+ await fs.ensureDir(path.dirname(tempBackupPath));
533
+ await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
534
+ }
535
+ }
536
+
537
+ return { tempBackupDir, tempModifiedBackupDir };
538
+ }
539
+
540
+ /**
541
+ * Install official (non-custom) modules.
542
+ * @param {Object} config - Installation configuration
543
+ * @param {Object} paths - InstallPaths instance
544
+ * @param {string[]} officialModuleIds - Official module IDs to install
545
+ * @param {Function} addResult - Callback to record installation results
546
+ * @param {boolean} isQuickUpdate - Whether this is a quick update
547
+ * @param {Object} ctx - Shared context: { message, installedModuleNames }
548
+ */
549
+ async _installOfficialModules(config, paths, officialModuleIds, addResult, isQuickUpdate, officialModules, ctx) {
550
+ const { message, installedModuleNames } = ctx;
551
+
552
+ for (const moduleName of officialModuleIds) {
553
+ if (installedModuleNames.has(moduleName)) continue;
554
+ installedModuleNames.add(moduleName);
555
+
556
+ message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
557
+
558
+ const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
559
+ await officialModules.install(
560
+ moduleName,
561
+ paths.scmDir,
562
+ (filePath) => {
563
+ this.installedFiles.add(filePath);
564
+ },
565
+ {
566
+ skipModuleInstaller: true,
567
+ moduleConfig: moduleConfig,
568
+ installer: this,
569
+ silent: true,
570
+ },
571
+ );
572
+
573
+ addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Install custom modules using CustomModules.install().
579
+ * Source paths come from this.customModules.paths (populated by discoverPaths).
580
+ */
581
+ async _installCustomModules(config, paths, addResult, officialModules, ctx) {
582
+ const { message, installedModuleNames } = ctx;
583
+ const isQuickUpdate = config.isQuickUpdate();
584
+
585
+ for (const [moduleName, sourcePath] of this.customModules.paths) {
586
+ if (installedModuleNames.has(moduleName)) continue;
587
+ installedModuleNames.add(moduleName);
588
+
589
+ message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
590
+
591
+ const collectedModuleConfig = officialModules.moduleConfigs[moduleName] || {};
592
+ const result = await this.customModules.install(moduleName, paths.scmDir, (filePath) => this.installedFiles.add(filePath), {
593
+ moduleConfig: collectedModuleConfig,
594
+ });
595
+
596
+ // Generate runtime config.yaml with merged values
597
+ await this.generateModuleConfigs(paths.scmDir, {
598
+ [moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
599
+ });
600
+
601
+ addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Read files-manifest.csv
607
+ * @param {string} scmDir - SCM installation directory
608
+ * @returns {Array} Array of file entries from files-manifest.csv
609
+ */
610
+ async readFilesManifest(scmDir) {
611
+ const filesManifestPath = path.join(scmDir, '_config', 'files-manifest.csv');
612
+ if (!(await fs.pathExists(filesManifestPath))) {
613
+ return [];
614
+ }
615
+
616
+ try {
617
+ const content = await fs.readFile(filesManifestPath, 'utf8');
618
+ const lines = content.split('\n');
619
+ const files = [];
620
+
621
+ for (let i = 1; i < lines.length; i++) {
622
+ // Skip header
623
+ const line = lines[i].trim();
624
+ if (!line) continue;
625
+
626
+ // Parse CSV line properly handling quoted values
627
+ const parts = [];
628
+ let current = '';
629
+ let inQuotes = false;
630
+
631
+ for (const char of line) {
632
+ if (char === '"') {
633
+ inQuotes = !inQuotes;
634
+ } else if (char === ',' && !inQuotes) {
635
+ parts.push(current);
636
+ current = '';
637
+ } else {
638
+ current += char;
639
+ }
640
+ }
641
+ parts.push(current); // Add last part
642
+
643
+ if (parts.length >= 4) {
644
+ files.push({
645
+ type: parts[0],
646
+ name: parts[1],
647
+ module: parts[2],
648
+ path: parts[3],
649
+ hash: parts[4] || null, // Hash may not exist in old manifests
650
+ });
651
+ }
652
+ }
653
+
654
+ return files;
655
+ } catch (error) {
656
+ await prompts.log.warn('Could not read files-manifest.csv: ' + error.message);
657
+ return [];
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Detect custom and modified files
663
+ * @param {string} scmDir - SCM installation directory
664
+ * @param {Array} existingFilesManifest - Previous files from files-manifest.csv
665
+ * @returns {Object} Object with customFiles and modifiedFiles arrays
666
+ */
667
+ async detectCustomFiles(scmDir, existingFilesManifest) {
668
+ const customFiles = [];
669
+ const modifiedFiles = [];
670
+
671
+ // Memory is always in _scm/_memory
672
+ const scmMemoryPath = '_memory';
673
+
674
+ // Check if the manifest has hashes - if not, we can't detect modifications
675
+ let manifestHasHashes = false;
676
+ if (existingFilesManifest && existingFilesManifest.length > 0) {
677
+ manifestHasHashes = existingFilesManifest.some((f) => f.hash);
678
+ }
679
+
680
+ // Build map of previously installed files from files-manifest.csv with their hashes
681
+ const installedFilesMap = new Map();
682
+ for (const fileEntry of existingFilesManifest) {
683
+ if (fileEntry.path) {
684
+ const absolutePath = path.join(scmDir, fileEntry.path);
685
+ installedFilesMap.set(path.normalize(absolutePath), {
686
+ hash: fileEntry.hash,
687
+ relativePath: fileEntry.path,
688
+ });
689
+ }
690
+ }
691
+
692
+ // Recursively scan scmDir for all files
693
+ const scanDirectory = async (dir) => {
694
+ try {
695
+ const entries = await fs.readdir(dir, { withFileTypes: true });
696
+ for (const entry of entries) {
697
+ const fullPath = path.join(dir, entry.name);
698
+
699
+ if (entry.isDirectory()) {
700
+ // Skip certain directories
701
+ if (entry.name === 'node_modules' || entry.name === '.git') {
702
+ continue;
703
+ }
704
+ await scanDirectory(fullPath);
705
+ } else if (entry.isFile()) {
706
+ const normalizedPath = path.normalize(fullPath);
707
+ const fileInfo = installedFilesMap.get(normalizedPath);
708
+
709
+ // Skip certain system files that are auto-generated
710
+ const relativePath = path.relative(scmDir, fullPath);
711
+ const fileName = path.basename(fullPath);
712
+
713
+ // Skip _config directory EXCEPT for modified agent customizations
714
+ if (relativePath.startsWith('_config/') || relativePath.startsWith('_config\\')) {
715
+ // Special handling for .customize.yaml files - only preserve if modified
716
+ if (relativePath.includes('/agents/') && fileName.endsWith('.customize.yaml')) {
717
+ // Check if the customization file has been modified from manifest
718
+ const manifestPath = path.join(scmDir, '_config', 'manifest.yaml');
719
+ if (await fs.pathExists(manifestPath)) {
720
+ const crypto = require('node:crypto');
721
+ const currentContent = await fs.readFile(fullPath, 'utf8');
722
+ const currentHash = crypto.createHash('sha256').update(currentContent).digest('hex');
723
+
724
+ const yaml = require('yaml');
725
+ const manifestContent = await fs.readFile(manifestPath, 'utf8');
726
+ const manifestData = yaml.parse(manifestContent);
727
+ const originalHash = manifestData.agentCustomizations?.[relativePath];
728
+
729
+ // Only add to customFiles if hash differs (user modified)
730
+ if (originalHash && currentHash !== originalHash) {
731
+ customFiles.push(fullPath);
732
+ }
733
+ }
734
+ }
735
+ continue;
736
+ }
737
+
738
+ if (relativePath.startsWith(scmMemoryPath + '/') && path.dirname(relativePath).includes('-sidecar')) {
739
+ continue;
740
+ }
741
+
742
+ // Skip config.yaml files - these are regenerated on each install/update
743
+ if (fileName === 'config.yaml') {
744
+ continue;
745
+ }
746
+
747
+ if (!fileInfo) {
748
+ // File not in manifest = custom file
749
+ // EXCEPT: Agent .md files in module folders are generated files, not custom
750
+ // Only treat .md files under _config/agents/ as custom
751
+ if (!(fileName.endsWith('.md') && relativePath.includes('/agents/') && !relativePath.startsWith('_config/'))) {
752
+ customFiles.push(fullPath);
753
+ }
754
+ } else if (manifestHasHashes && fileInfo.hash) {
755
+ // File in manifest with hash - check if it was modified
756
+ const currentHash = await this.manifest.calculateFileHash(fullPath);
757
+ if (currentHash && currentHash !== fileInfo.hash) {
758
+ // Hash changed = file was modified
759
+ modifiedFiles.push({
760
+ path: fullPath,
761
+ relativePath: fileInfo.relativePath,
762
+ });
763
+ }
764
+ }
765
+ }
766
+ }
767
+ } catch {
768
+ // Ignore errors scanning directories
769
+ }
770
+ };
771
+
772
+ await scanDirectory(scmDir);
773
+ return { customFiles, modifiedFiles };
774
+ }
775
+
776
+ /**
777
+ * Generate clean config.yaml files for each installed module
778
+ * @param {string} scmDir - SCM installation directory
779
+ * @param {Object} moduleConfigs - Collected configuration values
780
+ */
781
+ async generateModuleConfigs(scmDir, moduleConfigs) {
782
+ const yaml = require('yaml');
783
+
784
+ // Extract core config values to share with other modules
785
+ const coreConfig = moduleConfigs.core || {};
786
+
787
+ // Get all installed module directories
788
+ const entries = await fs.readdir(scmDir, { withFileTypes: true });
789
+ const installedModules = entries
790
+ .filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs')
791
+ .map((entry) => entry.name);
792
+
793
+ // Generate config.yaml for each installed module
794
+ for (const moduleName of installedModules) {
795
+ const modulePath = path.join(scmDir, moduleName);
796
+
797
+ // Get module-specific config or use empty object if none
798
+ const config = moduleConfigs[moduleName] || {};
799
+
800
+ if (await fs.pathExists(modulePath)) {
801
+ const configPath = path.join(modulePath, 'config.yaml');
802
+
803
+ // Create header
804
+ const packageJson = require(path.join(getProjectRoot(), 'package.json'));
805
+ const header = `# ${moduleName.toUpperCase()} Module Configuration
806
+ # Generated by SCM installer
807
+ # Version: ${packageJson.version}
808
+ # Date: ${new Date().toISOString()}
809
+
810
+ `;
811
+
812
+ // For non-core modules, add core config values directly
813
+ let finalConfig = { ...config };
814
+ let coreSection = '';
815
+
816
+ if (moduleName !== 'core' && coreConfig && Object.keys(coreConfig).length > 0) {
817
+ // Add core values directly to the module config
818
+ // These will be available for reference in the module
819
+ finalConfig = {
820
+ ...config,
821
+ ...coreConfig, // Spread core config values directly into the module config
822
+ };
823
+
824
+ // Create a comment section to identify core values
825
+ coreSection = '\n# Core Configuration Values\n';
826
+ }
827
+
828
+ // Clean the config to remove any non-serializable values (like functions)
829
+ const cleanConfig = structuredClone(finalConfig);
830
+
831
+ // Convert config to YAML
832
+ let yamlContent = yaml.stringify(cleanConfig, {
833
+ indent: 2,
834
+ lineWidth: 0,
835
+ minContentWidth: 0,
836
+ });
837
+
838
+ // If we have core values, reorganize the YAML to group them with their comment
839
+ if (coreSection && moduleName !== 'core') {
840
+ // Split the YAML into lines
841
+ const lines = yamlContent.split('\n');
842
+ const moduleConfigLines = [];
843
+ const coreConfigLines = [];
844
+
845
+ // Separate module-specific and core config lines
846
+ for (const line of lines) {
847
+ const key = line.split(':')[0].trim();
848
+ if (Object.prototype.hasOwnProperty.call(coreConfig, key)) {
849
+ coreConfigLines.push(line);
850
+ } else {
851
+ moduleConfigLines.push(line);
852
+ }
853
+ }
854
+
855
+ // Rebuild YAML with module config first, then core config with comment
856
+ yamlContent = moduleConfigLines.join('\n');
857
+ if (coreConfigLines.length > 0) {
858
+ yamlContent += coreSection + coreConfigLines.join('\n');
859
+ }
860
+ }
861
+
862
+ // Write the clean config file with POSIX-compliant final newline
863
+ const content = header + yamlContent;
864
+ await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8');
865
+
866
+ // Track the config file in installedFiles
867
+ this.installedFiles.add(configPath);
868
+ }
869
+ }
870
+ }
871
+
872
+ /**
873
+ * Merge all module-help.csv files into a single scm-help.csv
874
+ * Scans all installed modules for module-help.csv and merges them
875
+ * Enriches agent info from agent-manifest.csv
876
+ * Output is written to _scm/_config/scm-help.csv
877
+ * @param {string} scmDir - SCM installation directory
878
+ */
879
+ async mergeModuleHelpCatalogs(scmDir) {
880
+ const allRows = [];
881
+ const headerRow =
882
+ 'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs';
883
+
884
+ // Load agent manifest for agent info lookup
885
+ const agentManifestPath = path.join(scmDir, '_config', 'agent-manifest.csv');
886
+ const agentInfo = new Map(); // agent-name -> {command, displayName, title+icon}
887
+
888
+ if (await fs.pathExists(agentManifestPath)) {
889
+ const manifestContent = await fs.readFile(agentManifestPath, 'utf8');
890
+ const lines = manifestContent.split('\n').filter((line) => line.trim());
891
+
892
+ for (const line of lines) {
893
+ if (line.startsWith('name,')) continue; // Skip header
894
+
895
+ const cols = line.split(',');
896
+ if (cols.length >= 4) {
897
+ const agentName = cols[0].replaceAll('"', '').trim();
898
+ const displayName = cols[1].replaceAll('"', '').trim();
899
+ const title = cols[2].replaceAll('"', '').trim();
900
+ const icon = cols[3].replaceAll('"', '').trim();
901
+ const module = cols[10] ? cols[10].replaceAll('"', '').trim() : '';
902
+
903
+ // Build agent command: scm:module:agent:name
904
+ const agentCommand = module ? `scm:${module}:agent:${agentName}` : `scm:agent:${agentName}`;
905
+
906
+ agentInfo.set(agentName, {
907
+ command: agentCommand,
908
+ displayName: displayName || agentName,
909
+ title: icon && title ? `${icon} ${title}` : title || agentName,
910
+ });
911
+ }
912
+ }
913
+ }
914
+
915
+ // Get all installed module directories
916
+ const entries = await fs.readdir(scmDir, { withFileTypes: true });
917
+ const installedModules = entries
918
+ .filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs' && entry.name !== '_memory')
919
+ .map((entry) => entry.name);
920
+
921
+ // Add core module to scan (it's installed at root level as _config, but we check src/core-skills)
922
+ const coreModulePath = getSourcePath('core-skills');
923
+ const modulePaths = new Map();
924
+
925
+ // Map all module source paths
926
+ if (await fs.pathExists(coreModulePath)) {
927
+ modulePaths.set('core', coreModulePath);
928
+ }
929
+
930
+ // Map installed module paths
931
+ for (const moduleName of installedModules) {
932
+ const modulePath = path.join(scmDir, moduleName);
933
+ modulePaths.set(moduleName, modulePath);
934
+ }
935
+
936
+ // Scan each module for module-help.csv
937
+ for (const [moduleName, modulePath] of modulePaths) {
938
+ const helpFilePath = path.join(modulePath, 'module-help.csv');
939
+
940
+ if (await fs.pathExists(helpFilePath)) {
941
+ try {
942
+ const content = await fs.readFile(helpFilePath, 'utf8');
943
+ const lines = content.split('\n').filter((line) => line.trim() && !line.startsWith('#'));
944
+
945
+ for (const line of lines) {
946
+ // Skip header row
947
+ if (line.startsWith('module,')) {
948
+ continue;
949
+ }
950
+
951
+ // Parse the line - handle quoted fields with commas
952
+ const columns = this.parseCSVLine(line);
953
+ if (columns.length >= 12) {
954
+ // Map old schema to new schema
955
+ // Old: module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
956
+ // New: module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs
957
+
958
+ const [
959
+ module,
960
+ phase,
961
+ name,
962
+ code,
963
+ sequence,
964
+ workflowFile,
965
+ command,
966
+ required,
967
+ agentName,
968
+ options,
969
+ description,
970
+ outputLocation,
971
+ outputs,
972
+ ] = columns;
973
+
974
+ // If module column is empty, set it to this module's name (except for core which stays empty for universal tools)
975
+ const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
976
+
977
+ // Lookup agent info
978
+ const cleanAgentName = agentName ? agentName.trim() : '';
979
+ const agentData = agentInfo.get(cleanAgentName) || { command: '', displayName: '', title: '' };
980
+
981
+ // Build new row with agent info
982
+ const newRow = [
983
+ finalModule,
984
+ phase || '',
985
+ name || '',
986
+ code || '',
987
+ sequence || '',
988
+ workflowFile || '',
989
+ command || '',
990
+ required || 'false',
991
+ cleanAgentName,
992
+ agentData.command,
993
+ agentData.displayName,
994
+ agentData.title,
995
+ options || '',
996
+ description || '',
997
+ outputLocation || '',
998
+ outputs || '',
999
+ ];
1000
+
1001
+ allRows.push(newRow.map((c) => this.escapeCSVField(c)).join(','));
1002
+ }
1003
+ }
1004
+
1005
+ if (process.env.SCM_VERBOSE_INSTALL === 'true') {
1006
+ await prompts.log.message(` Merged module-help from: ${moduleName}`);
1007
+ }
1008
+ } catch (error) {
1009
+ await prompts.log.warn(` Warning: Failed to read module-help.csv from ${moduleName}: ${error.message}`);
1010
+ }
1011
+ }
1012
+ }
1013
+
1014
+ // Sort by module, then phase, then sequence
1015
+ allRows.sort((a, b) => {
1016
+ const colsA = this.parseCSVLine(a);
1017
+ const colsB = this.parseCSVLine(b);
1018
+
1019
+ // Module comparison (empty module/universal tools come first)
1020
+ const moduleA = (colsA[0] || '').toLowerCase();
1021
+ const moduleB = (colsB[0] || '').toLowerCase();
1022
+ if (moduleA !== moduleB) {
1023
+ return moduleA.localeCompare(moduleB);
1024
+ }
1025
+
1026
+ // Phase comparison
1027
+ const phaseA = colsA[1] || '';
1028
+ const phaseB = colsB[1] || '';
1029
+ if (phaseA !== phaseB) {
1030
+ return phaseA.localeCompare(phaseB);
1031
+ }
1032
+
1033
+ // Sequence comparison
1034
+ const seqA = parseInt(colsA[4] || '0', 10);
1035
+ const seqB = parseInt(colsB[4] || '0', 10);
1036
+ return seqA - seqB;
1037
+ });
1038
+
1039
+ // Write merged catalog
1040
+ const outputDir = path.join(scmDir, '_config');
1041
+ await fs.ensureDir(outputDir);
1042
+ const outputPath = path.join(outputDir, 'scm-help.csv');
1043
+
1044
+ const mergedContent = [headerRow, ...allRows].join('\n');
1045
+ await fs.writeFile(outputPath, mergedContent, 'utf8');
1046
+
1047
+ // Track the installed file
1048
+ this.installedFiles.add(outputPath);
1049
+
1050
+ if (process.env.SCM_VERBOSE_INSTALL === 'true') {
1051
+ await prompts.log.message(` Generated scm-help.csv: ${allRows.length} workflows`);
1052
+ }
1053
+ }
1054
+
1055
+ /**
1056
+ * Render a consolidated install summary using prompts.note()
1057
+ * @param {Array} results - Array of {step, status: 'ok'|'error'|'warn', detail}
1058
+ * @param {Object} context - {scmDir, modules, ides, customFiles, modifiedFiles}
1059
+ */
1060
+ async renderInstallSummary(results, context = {}) {
1061
+ const color = await prompts.getColor();
1062
+ const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
1063
+
1064
+ // Build step lines with status indicators
1065
+ const lines = [];
1066
+ for (const r of results) {
1067
+ let stepLabel = null;
1068
+
1069
+ if (r.status !== 'ok') {
1070
+ stepLabel = r.step;
1071
+ } else if (r.step === 'Core') {
1072
+ stepLabel = 'SCM';
1073
+ } else if (r.step.startsWith('Module: ')) {
1074
+ stepLabel = r.step;
1075
+ } else if (selectedIdes.has(String(r.step).toLowerCase())) {
1076
+ stepLabel = r.step;
1077
+ }
1078
+
1079
+ if (!stepLabel) {
1080
+ continue;
1081
+ }
1082
+
1083
+ let icon;
1084
+ if (r.status === 'ok') {
1085
+ icon = color.green('\u2713');
1086
+ } else if (r.status === 'warn') {
1087
+ icon = color.yellow('!');
1088
+ } else {
1089
+ icon = color.red('\u2717');
1090
+ }
1091
+ const detail = r.detail ? color.dim(` (${r.detail})`) : '';
1092
+ lines.push(` ${icon} ${stepLabel}${detail}`);
1093
+ }
1094
+
1095
+ if ((context.ides || []).length === 0) {
1096
+ lines.push(` ${color.green('\u2713')} No IDE selected ${color.dim('(installed in _scm only)')}`);
1097
+ }
1098
+
1099
+ // Context and warnings
1100
+ lines.push('');
1101
+ if (context.scmDir) {
1102
+ lines.push(` Installed to: ${color.dim(context.scmDir)}`);
1103
+ }
1104
+ if (context.customFiles && context.customFiles.length > 0) {
1105
+ lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
1106
+ }
1107
+ if (context.modifiedFiles && context.modifiedFiles.length > 0) {
1108
+ lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`);
1109
+ }
1110
+
1111
+ // Next steps
1112
+ lines.push(
1113
+ '',
1114
+ ' Next steps:',
1115
+ ` Read our new Docs Site: ${color.dim('https://docs.scm-method.org/')}`,
1116
+ ` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
1117
+ ` Star us on GitHub: ${color.dim('https://github.com/scm-code-org/SCM-METHOD/')}`,
1118
+ ` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@SCMCode')}`,
1119
+ );
1120
+ if (context.ides && context.ides.length > 0) {
1121
+ lines.push(` Invoke the ${color.cyan('scm-help')} skill in your IDE Agent to get started`);
1122
+ }
1123
+
1124
+ await prompts.note(lines.join('\n'), 'SCM is ready to use!');
1125
+ }
1126
+
1127
+ /**
1128
+ * Quick update method - preserves all settings and only prompts for new config fields
1129
+ * @param {Object} config - Configuration with directory
1130
+ * @returns {Object} Update result
1131
+ */
1132
+ async quickUpdate(config) {
1133
+ const projectDir = path.resolve(config.directory);
1134
+ const { scmDir } = await this.findBmadDir(projectDir);
1135
+
1136
+ // Check if scm directory exists
1137
+ if (!(await fs.pathExists(scmDir))) {
1138
+ throw new Error(`SCM not installed at ${scmDir}. Use regular install for first-time setup.`);
1139
+ }
1140
+
1141
+ // Detect existing installation
1142
+ const existingInstall = await ExistingInstall.detect(scmDir);
1143
+ const installedModules = existingInstall.moduleIds;
1144
+ const configuredIdes = existingInstall.ides;
1145
+ const projectRoot = path.dirname(scmDir);
1146
+
1147
+ // Get custom module sources: first from --custom-content (re-cache from source), then from cache
1148
+ const customModuleSources = new Map();
1149
+ if (config.customContent?.sources?.length > 0) {
1150
+ for (const source of config.customContent.sources) {
1151
+ if (source.id && source.path && (await fs.pathExists(source.path))) {
1152
+ customModuleSources.set(source.id, {
1153
+ id: source.id,
1154
+ name: source.name || source.id,
1155
+ sourcePath: source.path,
1156
+ cached: false, // From CLI, will be re-cached
1157
+ });
1158
+ }
1159
+ }
1160
+ }
1161
+ const cacheDir = path.join(scmDir, '_config', 'custom');
1162
+ if (await fs.pathExists(cacheDir)) {
1163
+ const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
1164
+
1165
+ for (const cachedModule of cachedModules) {
1166
+ const moduleId = cachedModule.name;
1167
+ const cachedPath = path.join(cacheDir, moduleId);
1168
+
1169
+ // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
1170
+ if (!(await fs.pathExists(cachedPath))) {
1171
+ continue;
1172
+ }
1173
+ if (!cachedModule.isDirectory()) {
1174
+ continue;
1175
+ }
1176
+
1177
+ // Skip if we already have this module from manifest
1178
+ if (customModuleSources.has(moduleId)) {
1179
+ continue;
1180
+ }
1181
+
1182
+ // Check if this is an external official module - skip cache for those
1183
+ const isExternal = await this.externalModuleManager.hasModule(moduleId);
1184
+ if (isExternal) {
1185
+ continue;
1186
+ }
1187
+
1188
+ // Check if this is actually a custom module (has module.yaml)
1189
+ const moduleYamlPath = path.join(cachedPath, 'module.yaml');
1190
+ if (await fs.pathExists(moduleYamlPath)) {
1191
+ customModuleSources.set(moduleId, {
1192
+ id: moduleId,
1193
+ name: moduleId,
1194
+ sourcePath: cachedPath,
1195
+ cached: true,
1196
+ });
1197
+ }
1198
+ }
1199
+ }
1200
+
1201
+ // Get available modules (what we have source for)
1202
+ const availableModulesData = await new OfficialModules().listAvailable();
1203
+ const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
1204
+
1205
+ // Add external official modules to available modules
1206
+ const externalModules = await this.externalModuleManager.listAvailable();
1207
+ for (const externalModule of externalModules) {
1208
+ if (installedModules.includes(externalModule.code) && !availableModules.some((m) => m.id === externalModule.code)) {
1209
+ availableModules.push({
1210
+ id: externalModule.code,
1211
+ name: externalModule.name,
1212
+ isExternal: true,
1213
+ fromExternal: true,
1214
+ });
1215
+ }
1216
+ }
1217
+
1218
+ // Add custom modules from manifest if their sources exist
1219
+ for (const [moduleId, customModule] of customModuleSources) {
1220
+ const sourcePath = customModule.sourcePath;
1221
+ if (sourcePath && (await fs.pathExists(sourcePath)) && !availableModules.some((m) => m.id === moduleId)) {
1222
+ availableModules.push({
1223
+ id: moduleId,
1224
+ name: customModule.name || moduleId,
1225
+ path: sourcePath,
1226
+ isCustom: true,
1227
+ fromManifest: true,
1228
+ });
1229
+ }
1230
+ }
1231
+
1232
+ // Handle missing custom module sources
1233
+ const customModuleResult = await this.handleMissingCustomSources(
1234
+ customModuleSources,
1235
+ scmDir,
1236
+ projectRoot,
1237
+ 'update',
1238
+ installedModules,
1239
+ config.skipPrompts || false,
1240
+ );
1241
+
1242
+ const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
1243
+
1244
+ const customModulesFromManifest = validCustomModules.map((m) => ({
1245
+ ...m,
1246
+ isCustom: true,
1247
+ hasUpdate: true,
1248
+ }));
1249
+
1250
+ const allAvailableModules = [...availableModules, ...customModulesFromManifest];
1251
+ const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
1252
+
1253
+ // Only update modules that are BOTH installed AND available (we have source for)
1254
+ const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
1255
+ const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id));
1256
+
1257
+ // Add custom modules that were kept without sources to the skipped modules
1258
+ for (const keptModule of keptModulesWithoutSources) {
1259
+ if (!skippedModules.includes(keptModule)) {
1260
+ skippedModules.push(keptModule);
1261
+ }
1262
+ }
1263
+
1264
+ if (skippedModules.length > 0) {
1265
+ await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`);
1266
+ }
1267
+
1268
+ // Load existing configs and collect new fields (if any)
1269
+ await prompts.log.info('Checking for new configuration options...');
1270
+ const quickModules = new OfficialModules();
1271
+ await quickModules.loadExistingConfig(projectDir);
1272
+
1273
+ let promptedForNewFields = false;
1274
+
1275
+ const corePrompted = await quickModules.collectModuleConfigQuick('core', projectDir, true);
1276
+ if (corePrompted) {
1277
+ promptedForNewFields = true;
1278
+ }
1279
+
1280
+ for (const moduleName of modulesToUpdate) {
1281
+ const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
1282
+ if (modulePrompted) {
1283
+ promptedForNewFields = true;
1284
+ }
1285
+ }
1286
+
1287
+ if (!promptedForNewFields) {
1288
+ await prompts.log.success('All configuration is up to date, no new options to configure');
1289
+ }
1290
+
1291
+ quickModules.collectedConfig._meta = {
1292
+ version: require(path.join(getProjectRoot(), 'package.json')).version,
1293
+ installDate: new Date().toISOString(),
1294
+ lastModified: new Date().toISOString(),
1295
+ };
1296
+
1297
+ // Build config and delegate to install()
1298
+ const installConfig = {
1299
+ directory: projectDir,
1300
+ modules: modulesToUpdate,
1301
+ ides: configuredIdes,
1302
+ coreConfig: quickModules.collectedConfig.core,
1303
+ moduleConfigs: quickModules.collectedConfig,
1304
+ actionType: 'install',
1305
+ _quickUpdate: true,
1306
+ _preserveModules: skippedModules,
1307
+ _customModuleSources: customModuleSources,
1308
+ _existingModules: installedModules,
1309
+ customContent: config.customContent,
1310
+ };
1311
+
1312
+ await this.install(installConfig);
1313
+
1314
+ return {
1315
+ success: true,
1316
+ moduleCount: modulesToUpdate.length,
1317
+ hadNewFields: promptedForNewFields,
1318
+ modules: modulesToUpdate,
1319
+ skippedModules: skippedModules,
1320
+ ides: configuredIdes,
1321
+ };
1322
+ }
1323
+
1324
+ /**
1325
+ * Uninstall SCM with selective removal options
1326
+ * @param {string} directory - Project directory
1327
+ * @param {Object} options - Uninstall options
1328
+ * @param {boolean} [options.removeModules=true] - Remove _scm/ directory
1329
+ * @param {boolean} [options.removeIdeConfigs=true] - Remove IDE configurations
1330
+ * @param {boolean} [options.removeOutputFolder=false] - Remove user artifacts output folder
1331
+ * @returns {Object} Result with success status and removed components
1332
+ */
1333
+ async uninstall(directory, options = {}) {
1334
+ const projectDir = path.resolve(directory);
1335
+ const { scmDir } = await this.findBmadDir(projectDir);
1336
+
1337
+ if (!(await fs.pathExists(scmDir))) {
1338
+ return { success: false, reason: 'not-installed' };
1339
+ }
1340
+
1341
+ // 1. DETECT: Read state BEFORE deleting anything
1342
+ const existingInstall = await ExistingInstall.detect(scmDir);
1343
+ const outputFolder = await this._readOutputFolder(scmDir);
1344
+
1345
+ const removed = { modules: false, ideConfigs: false, outputFolder: false };
1346
+
1347
+ // 2. IDE CLEANUP (before _scm/ deletion so configs are accessible)
1348
+ if (options.removeIdeConfigs !== false) {
1349
+ await this.uninstallIdeConfigs(projectDir, existingInstall, { silent: options.silent });
1350
+ removed.ideConfigs = true;
1351
+ }
1352
+
1353
+ // 3. OUTPUT FOLDER (only if explicitly requested)
1354
+ if (options.removeOutputFolder === true && outputFolder) {
1355
+ removed.outputFolder = await this.uninstallOutputFolder(projectDir, outputFolder);
1356
+ }
1357
+
1358
+ // 4. SCM DIRECTORY (last, after everything that needs it)
1359
+ if (options.removeModules !== false) {
1360
+ removed.modules = await this.uninstallModules(projectDir);
1361
+ }
1362
+
1363
+ return { success: true, removed, version: existingInstall.installed ? existingInstall.version : null };
1364
+ }
1365
+
1366
+ /**
1367
+ * Uninstall IDE configurations only
1368
+ * @param {string} projectDir - Project directory
1369
+ * @param {Object} existingInstall - Detection result from detector.detect()
1370
+ * @param {Object} [options] - Options (e.g. { silent: true })
1371
+ * @returns {Promise<Object>} Results from IDE cleanup
1372
+ */
1373
+ async uninstallIdeConfigs(projectDir, existingInstall, options = {}) {
1374
+ await this.ideManager.ensureInitialized();
1375
+ const cleanupOptions = { isUninstall: true, silent: options.silent };
1376
+ const ideList = existingInstall.ides;
1377
+ if (ideList.length > 0) {
1378
+ return this.ideManager.cleanupByList(projectDir, ideList, cleanupOptions);
1379
+ }
1380
+ return this.ideManager.cleanup(projectDir, cleanupOptions);
1381
+ }
1382
+
1383
+ /**
1384
+ * Remove user artifacts output folder
1385
+ * @param {string} projectDir - Project directory
1386
+ * @param {string} outputFolder - Output folder name (relative)
1387
+ * @returns {Promise<boolean>} Whether the folder was removed
1388
+ */
1389
+ async uninstallOutputFolder(projectDir, outputFolder) {
1390
+ if (!outputFolder) return false;
1391
+ const resolvedProject = path.resolve(projectDir);
1392
+ const outputPath = path.resolve(resolvedProject, outputFolder);
1393
+ if (!outputPath.startsWith(resolvedProject + path.sep)) {
1394
+ return false;
1395
+ }
1396
+ if (await fs.pathExists(outputPath)) {
1397
+ await fs.remove(outputPath);
1398
+ return true;
1399
+ }
1400
+ return false;
1401
+ }
1402
+
1403
+ /**
1404
+ * Remove the _scm/ directory
1405
+ * @param {string} projectDir - Project directory
1406
+ * @returns {Promise<boolean>} Whether the directory was removed
1407
+ */
1408
+ async uninstallModules(projectDir) {
1409
+ const { scmDir } = await this.findBmadDir(projectDir);
1410
+ if (await fs.pathExists(scmDir)) {
1411
+ await fs.remove(scmDir);
1412
+ return true;
1413
+ }
1414
+ return false;
1415
+ }
1416
+
1417
+ /**
1418
+ * Get installation status
1419
+ */
1420
+ async getStatus(directory) {
1421
+ const projectDir = path.resolve(directory);
1422
+ const { scmDir } = await this.findBmadDir(projectDir);
1423
+ return await ExistingInstall.detect(scmDir);
1424
+ }
1425
+
1426
+ /**
1427
+ * Get available modules
1428
+ */
1429
+ async getAvailableModules() {
1430
+ return await new OfficialModules().listAvailable();
1431
+ }
1432
+
1433
+ /**
1434
+ * Get the configured output folder name for a project
1435
+ * Resolves scmDir internally from projectDir
1436
+ * @param {string} projectDir - Project directory
1437
+ * @returns {string} Output folder name (relative, default: '_scm-output')
1438
+ */
1439
+ async getOutputFolder(projectDir) {
1440
+ const { scmDir } = await this.findBmadDir(projectDir);
1441
+ return this._readOutputFolder(scmDir);
1442
+ }
1443
+
1444
+ /**
1445
+ * Handle missing custom module sources interactively
1446
+ * @param {Map} customModuleSources - Map of custom module ID to info
1447
+ * @param {string} scmDir - SCM directory
1448
+ * @param {string} projectRoot - Project root directory
1449
+ * @param {string} operation - Current operation ('update', 'compile', etc.)
1450
+ * @param {Array} installedModules - Array of installed module IDs (will be modified)
1451
+ * @param {boolean} [skipPrompts=false] - Skip interactive prompts and keep all modules with missing sources
1452
+ * @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
1453
+ */
1454
+ async handleMissingCustomSources(customModuleSources, scmDir, projectRoot, operation, installedModules, skipPrompts = false) {
1455
+ const validCustomModules = [];
1456
+ const keptModulesWithoutSources = []; // Track modules kept without sources
1457
+ const customModulesWithMissingSources = [];
1458
+
1459
+ // Check which sources exist
1460
+ for (const [moduleId, customInfo] of customModuleSources) {
1461
+ if (await fs.pathExists(customInfo.sourcePath)) {
1462
+ validCustomModules.push({
1463
+ id: moduleId,
1464
+ name: customInfo.name,
1465
+ path: customInfo.sourcePath,
1466
+ info: customInfo,
1467
+ });
1468
+ } else {
1469
+ // For cached modules that are missing, we just skip them without prompting
1470
+ if (customInfo.cached) {
1471
+ // Skip cached modules without prompting
1472
+ keptModulesWithoutSources.push({
1473
+ id: moduleId,
1474
+ name: customInfo.name,
1475
+ cached: true,
1476
+ });
1477
+ } else {
1478
+ customModulesWithMissingSources.push({
1479
+ id: moduleId,
1480
+ name: customInfo.name,
1481
+ sourcePath: customInfo.sourcePath,
1482
+ relativePath: customInfo.relativePath,
1483
+ info: customInfo,
1484
+ });
1485
+ }
1486
+ }
1487
+ }
1488
+
1489
+ // If no missing sources, return immediately
1490
+ if (customModulesWithMissingSources.length === 0) {
1491
+ return {
1492
+ validCustomModules,
1493
+ keptModulesWithoutSources: [],
1494
+ };
1495
+ }
1496
+
1497
+ // Non-interactive mode: keep all modules with missing sources
1498
+ if (skipPrompts) {
1499
+ for (const missing of customModulesWithMissingSources) {
1500
+ keptModulesWithoutSources.push(missing.id);
1501
+ }
1502
+ return { validCustomModules, keptModulesWithoutSources };
1503
+ }
1504
+
1505
+ await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`);
1506
+
1507
+ let keptCount = 0;
1508
+ let updatedCount = 0;
1509
+ let removedCount = 0;
1510
+
1511
+ for (const missing of customModulesWithMissingSources) {
1512
+ await prompts.log.message(
1513
+ `${missing.name} (${missing.id})\n Original source: ${missing.relativePath}\n Full path: ${missing.sourcePath}`,
1514
+ );
1515
+
1516
+ const choices = [
1517
+ {
1518
+ name: 'Keep installed (will not be processed)',
1519
+ value: 'keep',
1520
+ hint: 'Keep',
1521
+ },
1522
+ {
1523
+ name: 'Specify new source location',
1524
+ value: 'update',
1525
+ hint: 'Update',
1526
+ },
1527
+ ];
1528
+
1529
+ // Only add remove option if not just compiling agents
1530
+ if (operation !== 'compile-agents') {
1531
+ choices.push({
1532
+ name: '⚠️ REMOVE module completely (destructive!)',
1533
+ value: 'remove',
1534
+ hint: 'Remove',
1535
+ });
1536
+ }
1537
+
1538
+ const action = await prompts.select({
1539
+ message: `How would you like to handle "${missing.name}"?`,
1540
+ choices,
1541
+ });
1542
+
1543
+ switch (action) {
1544
+ case 'update': {
1545
+ // Use sync validation because @clack/prompts doesn't support async validate
1546
+ const newSourcePath = await prompts.text({
1547
+ message: 'Enter the new path to the custom module:',
1548
+ default: missing.sourcePath,
1549
+ validate: (input) => {
1550
+ if (!input || input.trim() === '') {
1551
+ return 'Please enter a path';
1552
+ }
1553
+ const expandedPath = path.resolve(input.trim());
1554
+ if (!fs.pathExistsSync(expandedPath)) {
1555
+ return 'Path does not exist';
1556
+ }
1557
+ // Check if it looks like a valid module
1558
+ const moduleYamlPath = path.join(expandedPath, 'module.yaml');
1559
+ const agentsPath = path.join(expandedPath, 'agents');
1560
+ const workflowsPath = path.join(expandedPath, 'workflows');
1561
+
1562
+ if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
1563
+ return 'Path does not appear to contain a valid custom module';
1564
+ }
1565
+ return; // clack expects undefined for valid input
1566
+ },
1567
+ });
1568
+
1569
+ // Defensive: handleCancel should have exited, but guard against symbol propagation
1570
+ if (typeof newSourcePath !== 'string') {
1571
+ keptCount++;
1572
+ keptModulesWithoutSources.push(missing.id);
1573
+ continue;
1574
+ }
1575
+
1576
+ // Update the source in manifest
1577
+ const resolvedPath = path.resolve(newSourcePath.trim());
1578
+ missing.info.sourcePath = resolvedPath;
1579
+ // Remove relativePath - we only store absolute sourcePath now
1580
+ delete missing.info.relativePath;
1581
+ await this.manifest.addCustomModule(scmDir, missing.info);
1582
+
1583
+ validCustomModules.push({
1584
+ id: missing.id,
1585
+ name: missing.name,
1586
+ path: resolvedPath,
1587
+ info: missing.info,
1588
+ });
1589
+
1590
+ updatedCount++;
1591
+ await prompts.log.success('Updated source location');
1592
+
1593
+ break;
1594
+ }
1595
+ case 'remove': {
1596
+ // Extra confirmation for destructive remove
1597
+ await prompts.log.error(
1598
+ `WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!\n Module location: ${path.join(scmDir, missing.id)}`,
1599
+ );
1600
+
1601
+ const confirmDelete = await prompts.confirm({
1602
+ message: 'Are you absolutely sure you want to delete this module?',
1603
+ default: false,
1604
+ });
1605
+
1606
+ if (confirmDelete) {
1607
+ const typedConfirm = await prompts.text({
1608
+ message: 'Type "DELETE" to confirm permanent deletion:',
1609
+ validate: (input) => {
1610
+ if (input !== 'DELETE') {
1611
+ return 'You must type "DELETE" exactly to proceed';
1612
+ }
1613
+ return; // clack expects undefined for valid input
1614
+ },
1615
+ });
1616
+
1617
+ if (typedConfirm === 'DELETE') {
1618
+ // Remove the module from filesystem and manifest
1619
+ const modulePath = path.join(scmDir, missing.id);
1620
+ if (await fs.pathExists(modulePath)) {
1621
+ const fsExtra = require('fs-extra');
1622
+ await fsExtra.remove(modulePath);
1623
+ await prompts.log.warn(`Deleted module directory: ${path.relative(projectRoot, modulePath)}`);
1624
+ }
1625
+
1626
+ await this.manifest.removeModule(scmDir, missing.id);
1627
+ await this.manifest.removeCustomModule(scmDir, missing.id);
1628
+ await prompts.log.warn('Removed from manifest');
1629
+
1630
+ // Also remove from installedModules list
1631
+ if (installedModules && installedModules.includes(missing.id)) {
1632
+ const index = installedModules.indexOf(missing.id);
1633
+ if (index !== -1) {
1634
+ installedModules.splice(index, 1);
1635
+ }
1636
+ }
1637
+
1638
+ removedCount++;
1639
+ await prompts.log.error(`"${missing.name}" has been permanently removed`);
1640
+ } else {
1641
+ await prompts.log.message('Removal cancelled - module will be kept');
1642
+ keptCount++;
1643
+ }
1644
+ } else {
1645
+ await prompts.log.message('Removal cancelled - module will be kept');
1646
+ keptCount++;
1647
+ }
1648
+
1649
+ break;
1650
+ }
1651
+ case 'keep': {
1652
+ keptCount++;
1653
+ keptModulesWithoutSources.push(missing.id);
1654
+ await prompts.log.message('Module will be kept as-is');
1655
+
1656
+ break;
1657
+ }
1658
+ // No default
1659
+ }
1660
+ }
1661
+
1662
+ // Show summary
1663
+ if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
1664
+ let summary = 'Summary for custom modules with missing sources:';
1665
+ if (keptCount > 0) summary += `\n • ${keptCount} module(s) kept as-is`;
1666
+ if (updatedCount > 0) summary += `\n • ${updatedCount} module(s) updated with new sources`;
1667
+ if (removedCount > 0) summary += `\n • ${removedCount} module(s) permanently deleted`;
1668
+ await prompts.log.message(summary);
1669
+ }
1670
+
1671
+ return {
1672
+ validCustomModules,
1673
+ keptModulesWithoutSources,
1674
+ };
1675
+ }
1676
+
1677
+ /**
1678
+ * Find the scm installation directory in a project
1679
+ * Always uses the standard _scm folder name
1680
+ * @param {string} projectDir - Project directory
1681
+ * @returns {Promise<Object>} { scmDir: string }
1682
+ */
1683
+ async findBmadDir(projectDir) {
1684
+ const scmDir = path.join(projectDir, SCM_FOLDER_NAME);
1685
+ return { scmDir };
1686
+ }
1687
+
1688
+ /**
1689
+ * Read the output_folder setting from module config files
1690
+ * Checks bmm/config.yaml first, then other module configs
1691
+ * @param {string} scmDir - SCM installation directory
1692
+ * @returns {string} Output folder path or default
1693
+ */
1694
+ async _readOutputFolder(scmDir) {
1695
+ const yaml = require('yaml');
1696
+
1697
+ // Check bmm/config.yaml first (most common)
1698
+ const bmmConfigPath = path.join(scmDir, 'bmm', 'config.yaml');
1699
+ if (await fs.pathExists(bmmConfigPath)) {
1700
+ try {
1701
+ const content = await fs.readFile(bmmConfigPath, 'utf8');
1702
+ const config = yaml.parse(content);
1703
+ if (config && config.output_folder) {
1704
+ // Strip {project-root}/ prefix if present
1705
+ return config.output_folder.replace(/^\{project-root\}[/\\]/, '');
1706
+ }
1707
+ } catch {
1708
+ // Fall through to other modules
1709
+ }
1710
+ }
1711
+
1712
+ // Scan other module config.yaml files
1713
+ try {
1714
+ const entries = await fs.readdir(scmDir, { withFileTypes: true });
1715
+ for (const entry of entries) {
1716
+ if (!entry.isDirectory() || entry.name === 'bmm' || entry.name.startsWith('_')) continue;
1717
+ const configPath = path.join(scmDir, entry.name, 'config.yaml');
1718
+ if (await fs.pathExists(configPath)) {
1719
+ try {
1720
+ const content = await fs.readFile(configPath, 'utf8');
1721
+ const config = yaml.parse(content);
1722
+ if (config && config.output_folder) {
1723
+ return config.output_folder.replace(/^\{project-root\}[/\\]/, '');
1724
+ }
1725
+ } catch {
1726
+ // Continue scanning
1727
+ }
1728
+ }
1729
+ }
1730
+ } catch {
1731
+ // Directory scan failed
1732
+ }
1733
+
1734
+ // Default fallback
1735
+ return '_scm-output';
1736
+ }
1737
+
1738
+ /**
1739
+ * Parse a CSV line, handling quoted fields
1740
+ * @param {string} line - CSV line to parse
1741
+ * @returns {Array} Array of field values
1742
+ */
1743
+ parseCSVLine(line) {
1744
+ const result = [];
1745
+ let current = '';
1746
+ let inQuotes = false;
1747
+
1748
+ for (let i = 0; i < line.length; i++) {
1749
+ const char = line[i];
1750
+ const nextChar = line[i + 1];
1751
+
1752
+ if (char === '"') {
1753
+ if (inQuotes && nextChar === '"') {
1754
+ // Escaped quote
1755
+ current += '"';
1756
+ i++; // Skip next quote
1757
+ } else {
1758
+ // Toggle quote mode
1759
+ inQuotes = !inQuotes;
1760
+ }
1761
+ } else if (char === ',' && !inQuotes) {
1762
+ result.push(current);
1763
+ current = '';
1764
+ } else {
1765
+ current += char;
1766
+ }
1767
+ }
1768
+ result.push(current);
1769
+ return result;
1770
+ }
1771
+
1772
+ /**
1773
+ * Escape a CSV field if it contains special characters
1774
+ * @param {string} field - Field value to escape
1775
+ * @returns {string} Escaped field
1776
+ */
1777
+ escapeCSVField(field) {
1778
+ if (field === null || field === undefined) {
1779
+ return '';
1780
+ }
1781
+ const str = String(field);
1782
+ // If field contains comma, quote, or newline, wrap in quotes and escape inner quotes
1783
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
1784
+ return `"${str.replaceAll('"', '""')}"`;
1785
+ }
1786
+ return str;
1787
+ }
1788
+ }
1789
+
1790
+ module.exports = { Installer };