drt-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 (299) hide show
  1. package/AGENTS.md +45 -0
  2. package/GETTING-STARTED.md +269 -0
  3. package/README.md +233 -0
  4. package/drt-method-1.0.0.tgz +0 -0
  5. package/drt-modules.yaml +1 -0
  6. package/package.json +51 -0
  7. package/scripts/install-from-github.ps1 +22 -0
  8. package/scripts/install-from-github.sh +17 -0
  9. package/src/core-skills/drt-advanced-elicitation/SKILL.md +142 -0
  10. package/src/core-skills/drt-advanced-elicitation/methods.csv +72 -0
  11. package/src/core-skills/drt-brainstorming/SKILL.md +80 -0
  12. package/src/core-skills/drt-brainstorming/assets/brain-icons.json +166 -0
  13. package/src/core-skills/drt-brainstorming/assets/brain-methods.csv +109 -0
  14. package/src/core-skills/drt-brainstorming/assets/brain-selector.html +326 -0
  15. package/src/core-skills/drt-brainstorming/customize.toml +84 -0
  16. package/src/core-skills/drt-brainstorming/references/converge.md +24 -0
  17. package/src/core-skills/drt-brainstorming/references/finalize.md +26 -0
  18. package/src/core-skills/drt-brainstorming/references/headless.md +54 -0
  19. package/src/core-skills/drt-brainstorming/references/in-chat-techniques.md +18 -0
  20. package/src/core-skills/drt-brainstorming/references/mode-autonomous.md +10 -0
  21. package/src/core-skills/drt-brainstorming/references/mode-facilitator.md +11 -0
  22. package/src/core-skills/drt-brainstorming/references/mode-partner.md +16 -0
  23. package/src/core-skills/drt-brainstorming/references/resume.md +5 -0
  24. package/src/core-skills/drt-brainstorming/scripts/brain.py +740 -0
  25. package/src/core-skills/drt-customize/SKILL.md +111 -0
  26. package/src/core-skills/drt-customize/scripts/list_customizable_skills.py +231 -0
  27. package/src/core-skills/drt-editorial-review-prose/SKILL.md +86 -0
  28. package/src/core-skills/drt-editorial-review-structure/SKILL.md +179 -0
  29. package/src/core-skills/drt-forge-idea/SKILL.md +107 -0
  30. package/src/core-skills/drt-forge-idea/customize.toml +42 -0
  31. package/src/core-skills/drt-forge-idea/scripts/resolve_personas.py +270 -0
  32. package/src/core-skills/drt-help/SKILL.md +75 -0
  33. package/src/core-skills/drt-index-docs/SKILL.md +66 -0
  34. package/src/core-skills/drt-party-mode/SKILL.md +60 -0
  35. package/src/core-skills/drt-party-mode/customize.toml +211 -0
  36. package/src/core-skills/drt-party-mode/references/create-party.md +70 -0
  37. package/src/core-skills/drt-party-mode/references/mode-agent-team.md +13 -0
  38. package/src/core-skills/drt-party-mode/references/mode-auto.md +13 -0
  39. package/src/core-skills/drt-party-mode/references/mode-subagent.md +31 -0
  40. package/src/core-skills/drt-party-mode/references/party-memory.md +51 -0
  41. package/src/core-skills/drt-party-mode/scripts/resolve_party.py +272 -0
  42. package/src/core-skills/drt-review-adversarial-general/SKILL.md +37 -0
  43. package/src/core-skills/drt-review-edge-case-hunter/SKILL.md +73 -0
  44. package/src/core-skills/drt-review-edge-case-hunter/references/deletion-check.md +14 -0
  45. package/src/core-skills/drt-review-verification-gap/SKILL.md +106 -0
  46. package/src/core-skills/drt-shard-doc/SKILL.md +105 -0
  47. package/src/core-skills/drt-spec/SKILL.md +160 -0
  48. package/src/core-skills/drt-spec/assets/headless-schemas.md +33 -0
  49. package/src/core-skills/drt-spec/assets/spec-template.md +49 -0
  50. package/src/core-skills/drt-spec/assets/stories-schema.md +44 -0
  51. package/src/core-skills/drt-spec/customize.toml +53 -0
  52. package/src/core-skills/module-help.csv +14 -0
  53. package/src/core-skills/module.yaml +33 -0
  54. package/src/drt-skills/1-analysis/drt-agent-analyst/SKILL.md +76 -0
  55. package/src/drt-skills/1-analysis/drt-agent-analyst/customize.toml +90 -0
  56. package/src/drt-skills/1-analysis/drt-agent-tech-writer/SKILL.md +76 -0
  57. package/src/drt-skills/1-analysis/drt-agent-tech-writer/customize.toml +81 -0
  58. package/src/drt-skills/1-analysis/drt-agent-tech-writer/explain-concept.md +20 -0
  59. package/src/drt-skills/1-analysis/drt-agent-tech-writer/mermaid-gen.md +20 -0
  60. package/src/drt-skills/1-analysis/drt-agent-tech-writer/validate-doc.md +19 -0
  61. package/src/drt-skills/1-analysis/drt-agent-tech-writer/write-document.md +20 -0
  62. package/src/drt-skills/1-analysis/drt-document-project/SKILL.md +62 -0
  63. package/src/drt-skills/1-analysis/drt-document-project/checklist.md +245 -0
  64. package/src/drt-skills/1-analysis/drt-document-project/customize.toml +41 -0
  65. package/src/drt-skills/1-analysis/drt-document-project/documentation-requirements.csv +12 -0
  66. package/src/drt-skills/1-analysis/drt-document-project/instructions.md +128 -0
  67. package/src/drt-skills/1-analysis/drt-document-project/templates/deep-dive-template.md +345 -0
  68. package/src/drt-skills/1-analysis/drt-document-project/templates/index-template.md +169 -0
  69. package/src/drt-skills/1-analysis/drt-document-project/templates/project-overview-template.md +103 -0
  70. package/src/drt-skills/1-analysis/drt-document-project/templates/project-scan-report-schema.json +160 -0
  71. package/src/drt-skills/1-analysis/drt-document-project/templates/source-tree-template.md +135 -0
  72. package/src/drt-skills/1-analysis/drt-document-project/workflows/deep-dive-instructions.md +300 -0
  73. package/src/drt-skills/1-analysis/drt-document-project/workflows/deep-dive-workflow.md +34 -0
  74. package/src/drt-skills/1-analysis/drt-document-project/workflows/full-scan-instructions.md +1111 -0
  75. package/src/drt-skills/1-analysis/drt-document-project/workflows/full-scan-workflow.md +34 -0
  76. package/src/drt-skills/1-analysis/drt-prfaq/SKILL.md +135 -0
  77. package/src/drt-skills/1-analysis/drt-prfaq/agents/artifact-analyzer.md +60 -0
  78. package/src/drt-skills/1-analysis/drt-prfaq/agents/web-researcher.md +49 -0
  79. package/src/drt-skills/1-analysis/drt-prfaq/assets/prfaq-template.md +62 -0
  80. package/src/drt-skills/1-analysis/drt-prfaq/bmad-manifest.json +16 -0
  81. package/src/drt-skills/1-analysis/drt-prfaq/customize.toml +41 -0
  82. package/src/drt-skills/1-analysis/drt-prfaq/references/customer-faq.md +55 -0
  83. package/src/drt-skills/1-analysis/drt-prfaq/references/internal-faq.md +51 -0
  84. package/src/drt-skills/1-analysis/drt-prfaq/references/press-release.md +60 -0
  85. package/src/drt-skills/1-analysis/drt-prfaq/references/verdict.md +83 -0
  86. package/src/drt-skills/1-analysis/drt-product-brief/SKILL.md +91 -0
  87. package/src/drt-skills/1-analysis/drt-product-brief/assets/brief-template.md +41 -0
  88. package/src/drt-skills/1-analysis/drt-product-brief/customize.toml +99 -0
  89. package/src/drt-skills/1-analysis/research/drt-domain-research/SKILL.md +96 -0
  90. package/src/drt-skills/1-analysis/research/drt-domain-research/customize.toml +41 -0
  91. package/src/drt-skills/1-analysis/research/drt-domain-research/domain-steps/step-01-init.md +137 -0
  92. package/src/drt-skills/1-analysis/research/drt-domain-research/domain-steps/step-02-domain-analysis.md +229 -0
  93. package/src/drt-skills/1-analysis/research/drt-domain-research/domain-steps/step-03-competitive-landscape.md +238 -0
  94. package/src/drt-skills/1-analysis/research/drt-domain-research/domain-steps/step-04-regulatory-focus.md +206 -0
  95. package/src/drt-skills/1-analysis/research/drt-domain-research/domain-steps/step-05-technical-trends.md +234 -0
  96. package/src/drt-skills/1-analysis/research/drt-domain-research/domain-steps/step-06-research-synthesis.md +450 -0
  97. package/src/drt-skills/1-analysis/research/drt-domain-research/research.template.md +29 -0
  98. package/src/drt-skills/1-analysis/research/drt-market-research/SKILL.md +96 -0
  99. package/src/drt-skills/1-analysis/research/drt-market-research/customize.toml +41 -0
  100. package/src/drt-skills/1-analysis/research/drt-market-research/research.template.md +29 -0
  101. package/src/drt-skills/1-analysis/research/drt-market-research/steps/step-01-init.md +184 -0
  102. package/src/drt-skills/1-analysis/research/drt-market-research/steps/step-02-customer-behavior.md +239 -0
  103. package/src/drt-skills/1-analysis/research/drt-market-research/steps/step-03-customer-pain-points.md +251 -0
  104. package/src/drt-skills/1-analysis/research/drt-market-research/steps/step-04-customer-decisions.md +261 -0
  105. package/src/drt-skills/1-analysis/research/drt-market-research/steps/step-05-competitive-analysis.md +173 -0
  106. package/src/drt-skills/1-analysis/research/drt-market-research/steps/step-06-research-completion.md +484 -0
  107. package/src/drt-skills/1-analysis/research/drt-technical-research/SKILL.md +96 -0
  108. package/src/drt-skills/1-analysis/research/drt-technical-research/customize.toml +41 -0
  109. package/src/drt-skills/1-analysis/research/drt-technical-research/research.template.md +29 -0
  110. package/src/drt-skills/1-analysis/research/drt-technical-research/technical-steps/step-01-init.md +137 -0
  111. package/src/drt-skills/1-analysis/research/drt-technical-research/technical-steps/step-02-technical-overview.md +239 -0
  112. package/src/drt-skills/1-analysis/research/drt-technical-research/technical-steps/step-03-integration-patterns.md +248 -0
  113. package/src/drt-skills/1-analysis/research/drt-technical-research/technical-steps/step-04-architectural-patterns.md +202 -0
  114. package/src/drt-skills/1-analysis/research/drt-technical-research/technical-steps/step-05-implementation-research.md +233 -0
  115. package/src/drt-skills/1-analysis/research/drt-technical-research/technical-steps/step-06-research-synthesis.md +493 -0
  116. package/src/drt-skills/2-plan-workflows/drt-agent-pm/SKILL.md +76 -0
  117. package/src/drt-skills/2-plan-workflows/drt-agent-pm/customize.toml +75 -0
  118. package/src/drt-skills/2-plan-workflows/drt-agent-ux-designer/SKILL.md +76 -0
  119. package/src/drt-skills/2-plan-workflows/drt-agent-ux-designer/customize.toml +60 -0
  120. package/src/drt-skills/2-plan-workflows/drt-create-prd/SKILL.md +30 -0
  121. package/src/drt-skills/2-plan-workflows/drt-create-prd/customize.toml +41 -0
  122. package/src/drt-skills/2-plan-workflows/drt-edit-prd/SKILL.md +30 -0
  123. package/src/drt-skills/2-plan-workflows/drt-edit-prd/customize.toml +42 -0
  124. package/src/drt-skills/2-plan-workflows/drt-prd/SKILL.md +92 -0
  125. package/src/drt-skills/2-plan-workflows/drt-prd/assets/headless-schemas.md +76 -0
  126. package/src/drt-skills/2-plan-workflows/drt-prd/assets/prd-template.md +165 -0
  127. package/src/drt-skills/2-plan-workflows/drt-prd/assets/prd-validation-checklist.md +135 -0
  128. package/src/drt-skills/2-plan-workflows/drt-prd/assets/validation-report-template.html +325 -0
  129. package/src/drt-skills/2-plan-workflows/drt-prd/customize.toml +147 -0
  130. package/src/drt-skills/2-plan-workflows/drt-prd/references/headless.md +39 -0
  131. package/src/drt-skills/2-plan-workflows/drt-prd/references/validate.md +97 -0
  132. package/src/drt-skills/2-plan-workflows/drt-ux/SKILL.md +90 -0
  133. package/src/drt-skills/2-plan-workflows/drt-ux/assets/color-themes.md +9 -0
  134. package/src/drt-skills/2-plan-workflows/drt-ux/assets/design-directions.md +9 -0
  135. package/src/drt-skills/2-plan-workflows/drt-ux/assets/design-example-editorial.md +158 -0
  136. package/src/drt-skills/2-plan-workflows/drt-ux/assets/design-example-mobile.md +93 -0
  137. package/src/drt-skills/2-plan-workflows/drt-ux/assets/design-example-shadcn.md +109 -0
  138. package/src/drt-skills/2-plan-workflows/drt-ux/assets/excalidraw-wireframe.md +19 -0
  139. package/src/drt-skills/2-plan-workflows/drt-ux/assets/experience-example-mobile.md +112 -0
  140. package/src/drt-skills/2-plan-workflows/drt-ux/assets/experience-example-shadcn.md +133 -0
  141. package/src/drt-skills/2-plan-workflows/drt-ux/assets/headless-schemas.md +84 -0
  142. package/src/drt-skills/2-plan-workflows/drt-ux/assets/key-screens.md +29 -0
  143. package/src/drt-skills/2-plan-workflows/drt-ux/assets/validation-report-template.html +319 -0
  144. package/src/drt-skills/2-plan-workflows/drt-ux/customize.toml +100 -0
  145. package/src/drt-skills/2-plan-workflows/drt-ux/references/creative-tools.md +19 -0
  146. package/src/drt-skills/2-plan-workflows/drt-ux/references/design-md-spec.md +50 -0
  147. package/src/drt-skills/2-plan-workflows/drt-ux/references/headless.md +37 -0
  148. package/src/drt-skills/2-plan-workflows/drt-ux/references/validate.md +115 -0
  149. package/src/drt-skills/2-plan-workflows/drt-validate-prd/SKILL.md +30 -0
  150. package/src/drt-skills/2-plan-workflows/drt-validate-prd/customize.toml +42 -0
  151. package/src/drt-skills/3-solutioning/drt-agent-architect/SKILL.md +76 -0
  152. package/src/drt-skills/3-solutioning/drt-agent-architect/customize.toml +65 -0
  153. package/src/drt-skills/3-solutioning/drt-architecture/SKILL.md +85 -0
  154. package/src/drt-skills/3-solutioning/drt-architecture/assets/spine-template.md +79 -0
  155. package/src/drt-skills/3-solutioning/drt-architecture/customize.toml +100 -0
  156. package/src/drt-skills/3-solutioning/drt-architecture/references/headless.md +26 -0
  157. package/src/drt-skills/3-solutioning/drt-architecture/references/reviewer-gate.md +13 -0
  158. package/src/drt-skills/3-solutioning/drt-architecture/scripts/lint_spine.py +257 -0
  159. package/src/drt-skills/3-solutioning/drt-check-implementation-readiness/SKILL.md +91 -0
  160. package/src/drt-skills/3-solutioning/drt-check-implementation-readiness/customize.toml +41 -0
  161. package/src/drt-skills/3-solutioning/drt-check-implementation-readiness/steps/step-01-document-discovery.md +179 -0
  162. package/src/drt-skills/3-solutioning/drt-check-implementation-readiness/steps/step-02-prd-analysis.md +168 -0
  163. package/src/drt-skills/3-solutioning/drt-check-implementation-readiness/steps/step-03-epic-coverage-validation.md +169 -0
  164. package/src/drt-skills/3-solutioning/drt-check-implementation-readiness/steps/step-04-ux-alignment.md +129 -0
  165. package/src/drt-skills/3-solutioning/drt-check-implementation-readiness/steps/step-05-epic-quality-review.md +241 -0
  166. package/src/drt-skills/3-solutioning/drt-check-implementation-readiness/steps/step-06-final-assessment.md +132 -0
  167. package/src/drt-skills/3-solutioning/drt-check-implementation-readiness/templates/readiness-report-template.md +4 -0
  168. package/src/drt-skills/3-solutioning/drt-create-architecture/SKILL.md +30 -0
  169. package/src/drt-skills/3-solutioning/drt-create-architecture/customize.toml +41 -0
  170. package/src/drt-skills/3-solutioning/drt-create-epics-and-stories/SKILL.md +93 -0
  171. package/src/drt-skills/3-solutioning/drt-create-epics-and-stories/customize.toml +41 -0
  172. package/src/drt-skills/3-solutioning/drt-create-epics-and-stories/steps/step-01-validate-prerequisites.md +263 -0
  173. package/src/drt-skills/3-solutioning/drt-create-epics-and-stories/steps/step-02-design-epics.md +242 -0
  174. package/src/drt-skills/3-solutioning/drt-create-epics-and-stories/steps/step-03-create-stories.md +255 -0
  175. package/src/drt-skills/3-solutioning/drt-create-epics-and-stories/steps/step-04-final-validation.md +143 -0
  176. package/src/drt-skills/3-solutioning/drt-create-epics-and-stories/templates/epics-template.md +61 -0
  177. package/src/drt-skills/3-solutioning/drt-generate-project-context/SKILL.md +81 -0
  178. package/src/drt-skills/3-solutioning/drt-generate-project-context/customize.toml +41 -0
  179. package/src/drt-skills/3-solutioning/drt-generate-project-context/project-context-template.md +21 -0
  180. package/src/drt-skills/3-solutioning/drt-generate-project-context/steps/step-01-discover.md +186 -0
  181. package/src/drt-skills/3-solutioning/drt-generate-project-context/steps/step-02-generate.md +321 -0
  182. package/src/drt-skills/3-solutioning/drt-generate-project-context/steps/step-03-complete.md +284 -0
  183. package/src/drt-skills/4-implementation/drt-agent-dev/SKILL.md +76 -0
  184. package/src/drt-skills/4-implementation/drt-agent-dev/customize.toml +95 -0
  185. package/src/drt-skills/4-implementation/drt-checkpoint-preview/SKILL.md +68 -0
  186. package/src/drt-skills/4-implementation/drt-checkpoint-preview/customize.toml +41 -0
  187. package/src/drt-skills/4-implementation/drt-checkpoint-preview/generate-trail.md +38 -0
  188. package/src/drt-skills/4-implementation/drt-checkpoint-preview/step-01-orientation.md +105 -0
  189. package/src/drt-skills/4-implementation/drt-checkpoint-preview/step-02-walkthrough.md +89 -0
  190. package/src/drt-skills/4-implementation/drt-checkpoint-preview/step-03-detail-pass.md +106 -0
  191. package/src/drt-skills/4-implementation/drt-checkpoint-preview/step-04-testing.md +74 -0
  192. package/src/drt-skills/4-implementation/drt-checkpoint-preview/step-05-wrapup.md +30 -0
  193. package/src/drt-skills/4-implementation/drt-code-review/SKILL.md +90 -0
  194. package/src/drt-skills/4-implementation/drt-code-review/customize.toml +93 -0
  195. package/src/drt-skills/4-implementation/drt-code-review/steps/step-01-gather-context.md +84 -0
  196. package/src/drt-skills/4-implementation/drt-code-review/steps/step-02-review.md +32 -0
  197. package/src/drt-skills/4-implementation/drt-code-review/steps/step-03-triage.md +50 -0
  198. package/src/drt-skills/4-implementation/drt-code-review/steps/step-04-present.md +132 -0
  199. package/src/drt-skills/4-implementation/drt-correct-course/SKILL.md +301 -0
  200. package/src/drt-skills/4-implementation/drt-correct-course/checklist.md +288 -0
  201. package/src/drt-skills/4-implementation/drt-correct-course/customize.toml +41 -0
  202. package/src/drt-skills/4-implementation/drt-create-story/SKILL.md +432 -0
  203. package/src/drt-skills/4-implementation/drt-create-story/checklist.md +357 -0
  204. package/src/drt-skills/4-implementation/drt-create-story/customize.toml +41 -0
  205. package/src/drt-skills/4-implementation/drt-create-story/discover-inputs.md +88 -0
  206. package/src/drt-skills/4-implementation/drt-create-story/template.md +49 -0
  207. package/src/drt-skills/4-implementation/drt-dev-auto/SKILL.md +123 -0
  208. package/src/drt-skills/4-implementation/drt-dev-auto/compile-epic-context.md +62 -0
  209. package/src/drt-skills/4-implementation/drt-dev-auto/customize.toml +108 -0
  210. package/src/drt-skills/4-implementation/drt-dev-auto/spec-template.md +98 -0
  211. package/src/drt-skills/4-implementation/drt-dev-auto/step-01-clarify-and-route.md +84 -0
  212. package/src/drt-skills/4-implementation/drt-dev-auto/step-02-plan.md +31 -0
  213. package/src/drt-skills/4-implementation/drt-dev-auto/step-03-implement.md +42 -0
  214. package/src/drt-skills/4-implementation/drt-dev-auto/step-04-review.md +95 -0
  215. package/src/drt-skills/4-implementation/drt-dev-story/SKILL.md +500 -0
  216. package/src/drt-skills/4-implementation/drt-dev-story/checklist.md +80 -0
  217. package/src/drt-skills/4-implementation/drt-dev-story/customize.toml +41 -0
  218. package/src/drt-skills/4-implementation/drt-implement-task/SKILL.md +101 -0
  219. package/src/drt-skills/4-implementation/drt-implement-task/assets/task-spec-template.md +60 -0
  220. package/src/drt-skills/4-implementation/drt-implement-task/customize.toml +17 -0
  221. package/src/drt-skills/4-implementation/drt-implement-task/steps/step-01-gather-intent.md +42 -0
  222. package/src/drt-skills/4-implementation/drt-implement-task/steps/step-02-plan-and-approve.md +43 -0
  223. package/src/drt-skills/4-implementation/drt-implement-task/steps/step-03-confirm-dependencies.md +38 -0
  224. package/src/drt-skills/4-implementation/drt-implement-task/steps/step-04-implement.md +42 -0
  225. package/src/drt-skills/4-implementation/drt-implement-task/steps/step-05-qc-test.md +31 -0
  226. package/src/drt-skills/4-implementation/drt-implement-task/steps/step-06-quality-review.md +30 -0
  227. package/src/drt-skills/4-implementation/drt-implement-task/steps/step-07-fix-findings.md +33 -0
  228. package/src/drt-skills/4-implementation/drt-implement-task/steps/step-08-complete.md +35 -0
  229. package/src/drt-skills/4-implementation/drt-qa-generate-e2e-tests/SKILL.md +176 -0
  230. package/src/drt-skills/4-implementation/drt-qa-generate-e2e-tests/checklist.md +33 -0
  231. package/src/drt-skills/4-implementation/drt-qa-generate-e2e-tests/customize.toml +41 -0
  232. package/src/drt-skills/4-implementation/drt-quick-dev/SKILL.md +13 -0
  233. package/src/drt-skills/4-implementation/drt-quick-dev/compile-epic-context.md +62 -0
  234. package/src/drt-skills/4-implementation/drt-quick-dev/customize.toml +83 -0
  235. package/src/drt-skills/4-implementation/drt-quick-dev/render.js +206 -0
  236. package/src/drt-skills/4-implementation/drt-quick-dev/spec-template.md +89 -0
  237. package/src/drt-skills/4-implementation/drt-quick-dev/step-01-clarify-and-route.md +103 -0
  238. package/src/drt-skills/4-implementation/drt-quick-dev/step-02-plan.md +47 -0
  239. package/src/drt-skills/4-implementation/drt-quick-dev/step-03-implement.md +45 -0
  240. package/src/drt-skills/4-implementation/drt-quick-dev/step-04-review.md +55 -0
  241. package/src/drt-skills/4-implementation/drt-quick-dev/step-05-present.md +78 -0
  242. package/src/drt-skills/4-implementation/drt-quick-dev/step-oneshot.md +78 -0
  243. package/src/drt-skills/4-implementation/drt-quick-dev/sync-sprint-status.md +19 -0
  244. package/src/drt-skills/4-implementation/drt-quick-dev/workflow.md +85 -0
  245. package/src/drt-skills/4-implementation/drt-retrospective/SKILL.md +1527 -0
  246. package/src/drt-skills/4-implementation/drt-retrospective/customize.toml +41 -0
  247. package/src/drt-skills/4-implementation/drt-sprint-planning/SKILL.md +319 -0
  248. package/src/drt-skills/4-implementation/drt-sprint-planning/checklist.md +34 -0
  249. package/src/drt-skills/4-implementation/drt-sprint-planning/customize.toml +41 -0
  250. package/src/drt-skills/4-implementation/drt-sprint-planning/sprint-status-template.yaml +69 -0
  251. package/src/drt-skills/4-implementation/drt-sprint-status/SKILL.md +311 -0
  252. package/src/drt-skills/4-implementation/drt-sprint-status/customize.toml +41 -0
  253. package/src/drt-skills/module-help.csv +32 -0
  254. package/src/drt-skills/module.yaml +95 -0
  255. package/src/scripts/lib/load-toml.js +43 -0
  256. package/src/scripts/lib/merge-utils.js +89 -0
  257. package/src/scripts/memlog.js +184 -0
  258. package/src/scripts/package.json +7 -0
  259. package/src/scripts/resolve-config.js +56 -0
  260. package/src/scripts/resolve-customization.js +77 -0
  261. package/tools/installer/cli-utils.js +53 -0
  262. package/tools/installer/commands/install.js +149 -0
  263. package/tools/installer/commands/status.js +65 -0
  264. package/tools/installer/commands/uninstall.js +167 -0
  265. package/tools/installer/core/config.js +73 -0
  266. package/tools/installer/core/existing-install.js +121 -0
  267. package/tools/installer/core/install-paths.js +132 -0
  268. package/tools/installer/core/installer.js +1762 -0
  269. package/tools/installer/core/legacy-warnings.js +151 -0
  270. package/tools/installer/core/manifest-generator.js +859 -0
  271. package/tools/installer/core/manifest.js +433 -0
  272. package/tools/installer/core/uv-check.js +97 -0
  273. package/tools/installer/core/wsl-node-check.js +109 -0
  274. package/tools/installer/drt-cli.js +104 -0
  275. package/tools/installer/file-ops.js +204 -0
  276. package/tools/installer/fs-native.js +116 -0
  277. package/tools/installer/ide/_config-driven.js +972 -0
  278. package/tools/installer/ide/manager.js +324 -0
  279. package/tools/installer/ide/platform-codes.js +80 -0
  280. package/tools/installer/ide/platform-codes.yaml +343 -0
  281. package/tools/installer/ide/shared/installed-skills.js +50 -0
  282. package/tools/installer/ide/shared/path-utils.js +217 -0
  283. package/tools/installer/ide/shared/skill-manifest.js +57 -0
  284. package/tools/installer/install-messages.yaml +29 -0
  285. package/tools/installer/list-options.js +210 -0
  286. package/tools/installer/message-loader.js +83 -0
  287. package/tools/installer/modules/channel-plan.js +203 -0
  288. package/tools/installer/modules/channel-resolver.js +241 -0
  289. package/tools/installer/modules/custom-module-manager.js +918 -0
  290. package/tools/installer/modules/external-manager.js +667 -0
  291. package/tools/installer/modules/module-help-schema.js +13 -0
  292. package/tools/installer/modules/official-modules.js +2257 -0
  293. package/tools/installer/modules/plugin-resolver.js +398 -0
  294. package/tools/installer/modules/version-resolver.js +336 -0
  295. package/tools/installer/project-root.js +224 -0
  296. package/tools/installer/prompts.js +852 -0
  297. package/tools/installer/set-overrides.js +330 -0
  298. package/tools/installer/ui.js +2090 -0
  299. package/tools/installer/yaml-format.js +245 -0
@@ -0,0 +1,2090 @@
1
+ const path = require('node:path');
2
+ const os = require('node:os');
3
+ const semver = require('semver');
4
+ const fs = require('./fs-native');
5
+ const installerPackageJson = require('../../package.json');
6
+ const { CLIUtils } = require('./cli-utils');
7
+ const { ExternalModuleManager } = require('./modules/external-manager');
8
+ const { resolveModuleVersion } = require('./modules/version-resolver');
9
+ const { Manifest } = require('./core/manifest');
10
+ const {
11
+ parseChannelOptions,
12
+ buildPlan,
13
+ decideChannelForModule,
14
+ orphanPinWarnings,
15
+ bundledTargetWarnings,
16
+ } = require('./modules/channel-plan');
17
+ const channelResolver = require('./modules/channel-resolver');
18
+ const prompts = require('./prompts');
19
+ const { parseSetEntries } = require('./set-overrides');
20
+
21
+ const manifest = new Manifest();
22
+
23
+ /**
24
+ * Format a resolved version for display in installer labels.
25
+ * Semver-like values are normalized to a single leading "v".
26
+ * @param {string|null|undefined} version
27
+ * @returns {string}
28
+ */
29
+ function formatDisplayVersion(version) {
30
+ const trimmed = typeof version === 'string' ? version.trim() : '';
31
+ if (!trimmed) return '';
32
+
33
+ const normalized = semver.valid(semver.coerce(trimmed));
34
+ if (normalized) {
35
+ return `v${normalized}`;
36
+ }
37
+
38
+ return trimmed;
39
+ }
40
+
41
+ /**
42
+ * Build the display label for a module, showing an upgrade arrow when an
43
+ * installed semver differs from the latest resolvable semver.
44
+ * @param {string} name
45
+ * @param {string} latestVersion
46
+ * @param {string} installedVersion
47
+ * @returns {string}
48
+ */
49
+ function buildModuleLabel(name, latestVersion, installedVersion = '') {
50
+ const latestDisplay = formatDisplayVersion(latestVersion);
51
+ if (!latestDisplay) return name;
52
+
53
+ const installedDisplay = formatDisplayVersion(installedVersion);
54
+ const latestSemver = semver.valid(semver.coerce(latestVersion || ''));
55
+ const installedSemver = semver.valid(semver.coerce(installedVersion || ''));
56
+
57
+ if (installedDisplay && latestSemver && installedSemver && semver.neq(installedSemver, latestSemver)) {
58
+ return `${name} (${installedDisplay} → ${latestDisplay})`;
59
+ }
60
+
61
+ return `${name} (${latestDisplay})`;
62
+ }
63
+
64
+ /**
65
+ * Resolve the version to show for a module picker entry. External modules use
66
+ * the same channel/tag resolver as installs; bundled modules fall back to local
67
+ * source metadata.
68
+ * @param {string} moduleCode - Module code (e.g., 'core', 'drt', 'cis')
69
+ * @param {Object} options
70
+ * @param {string|null} [options.repoUrl] - Module repository URL for tag resolution
71
+ * @param {string|null} [options.registryDefault] - Registry default channel
72
+ * @param {Object|null} [options.channelOptions] - Parsed installer channel options
73
+ * @returns {Promise<{version: string, lookupAttempted: boolean, lookupSucceeded: boolean}>}
74
+ */
75
+ async function getModuleVersion(moduleCode, { repoUrl = null, registryDefault = null, channelOptions = null } = {}) {
76
+ if (repoUrl) {
77
+ const plan = decideChannelForModule({
78
+ code: moduleCode,
79
+ channelOptions,
80
+ registryDefault,
81
+ });
82
+
83
+ try {
84
+ const resolved = await channelResolver.resolveChannel({
85
+ channel: plan.channel,
86
+ pin: plan.pin,
87
+ repoUrl,
88
+ });
89
+ if (resolved?.version) {
90
+ return {
91
+ version: resolved.version,
92
+ lookupAttempted: plan.channel === 'stable',
93
+ lookupSucceeded: true,
94
+ };
95
+ }
96
+ } catch {
97
+ // Fall back to local metadata when tag resolution is unavailable.
98
+ }
99
+ }
100
+
101
+ const versionInfo = await resolveModuleVersion(moduleCode);
102
+ return {
103
+ version: versionInfo.version || '',
104
+ lookupAttempted: !!repoUrl,
105
+ lookupSucceeded: false,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * UI utilities for the installer
111
+ */
112
+ class UI {
113
+ async _retainUnavailableInstalledModules(selectedModules, installedModuleIds, drtDir, options = {}) {
114
+ const { OfficialModules } = require('./modules/official-modules');
115
+ const officialCodes = new Set(['core']);
116
+
117
+ const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
118
+ for (const mod of builtInModules) {
119
+ officialCodes.add(mod.id);
120
+ }
121
+
122
+ const externalManager = new ExternalModuleManager();
123
+ const registryModules = await externalManager.listAvailable();
124
+ for (const mod of registryModules) {
125
+ officialCodes.add(mod.code);
126
+ }
127
+
128
+ const { CustomModuleManager } = require('./modules/custom-module-manager');
129
+ const customMgr = new CustomModuleManager();
130
+ const selectedSet = new Set(selectedModules);
131
+ const preserveModules = [];
132
+
133
+ for (const moduleId of installedModuleIds) {
134
+ if (moduleId === 'core') continue;
135
+ if (!selectedSet.has(moduleId) && !options.preserveUnselected) continue;
136
+ // Resolve a possibly-renamed module code (e.g. `bauto` -> `drt-loop`)
137
+ // before checking availability, so a registry rename doesn't freeze
138
+ // the install here the way it would have prior to the alias support
139
+ // in ExternalModuleManager.getModuleByCode().
140
+ const canonicalId = await externalManager.resolveCanonicalCode(moduleId);
141
+ if (officialCodes.has(canonicalId)) continue;
142
+
143
+ const customSource = await customMgr.findModuleSourceByCode(moduleId, { drtDir });
144
+ if (!customSource) {
145
+ preserveModules.push(moduleId);
146
+ }
147
+ }
148
+
149
+ const preservedSet = new Set(preserveModules);
150
+ return {
151
+ selectedModules: selectedModules.filter((moduleId) => !preservedSet.has(moduleId)),
152
+ preserveModules,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Prompt for installation configuration
158
+ * @param {Object} options - Command-line options from install command
159
+ * @returns {Object} Installation configuration
160
+ */
161
+ async promptInstall(options = {}) {
162
+ await CLIUtils.displayLogo();
163
+
164
+ // Display version-specific start message from install-messages.yaml
165
+ const { MessageLoader } = require('./message-loader');
166
+ const messageLoader = new MessageLoader();
167
+ await messageLoader.displayStartMessage();
168
+
169
+ // Probe for `uv` before any other prompts: it's becoming the de facto
170
+ // runner for the Python scripts DRT workflows shell out to
171
+ // (`uv run <script>`), and uv provisions the interpreter itself, so it's
172
+ // the single thing worth checking for. The migration is still in progress
173
+ // (some skills still call `python3` directly), so this is informational —
174
+ // warn-don't-block, no ack prompt — and just points the user at setup
175
+ // (ideally "ask your agent to set up uv"). The installer runs in the
176
+ // destination environment, so probing PATH here tests the right machine.
177
+ const { checkUvEnvironment } = require('./core/uv-check');
178
+ await checkUvEnvironment();
179
+
180
+ // Parse channel flags (--channel/--all-*/--next=/--pin) once. Warnings
181
+ // are surfaced immediately so the user sees them before any git ops run.
182
+ const channelOptions = parseChannelOptions(options);
183
+ for (const warning of channelOptions.warnings) {
184
+ await prompts.log.warn(warning);
185
+ }
186
+
187
+ // When the user launched the installer from a prerelease (npx drt-method@next),
188
+ // mirror that intent for external modules: seed the global channel to 'next' so
189
+ // the module picker's version labels resolve from main HEAD (matching what
190
+ // actually gets installed) and the interactive channel gate skips — the user
191
+ // already declared "next" intent by typing @next. Explicit channel flags
192
+ // override this seed.
193
+ if (
194
+ semver.prerelease(installerPackageJson.version) !== null &&
195
+ !channelOptions.global &&
196
+ channelOptions.nextSet.size === 0 &&
197
+ channelOptions.pins.size === 0
198
+ ) {
199
+ channelOptions.global = 'next';
200
+ await prompts.log.info(
201
+ 'Launched from a prerelease — installing all external modules from main HEAD (next channel). Pass --all-stable or --pin to override.',
202
+ );
203
+ }
204
+
205
+ // Get directory from options or prompt
206
+ let confirmedDirectory;
207
+ if (options.directory) {
208
+ // Use provided directory from command-line
209
+ const expandedDir = this.expandUserPath(options.directory);
210
+ const validation = this.validateDirectorySync(expandedDir);
211
+ if (validation) {
212
+ throw new Error(`Invalid directory: ${validation}`);
213
+ }
214
+ confirmedDirectory = expandedDir;
215
+ await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`);
216
+ } else {
217
+ confirmedDirectory = await this.getConfirmedDirectory();
218
+ }
219
+
220
+ const { Installer } = require('./core/installer');
221
+ const installer = new Installer();
222
+ const { drtDir } = await installer.findDrtDir(confirmedDirectory);
223
+
224
+ // Check if there's an existing DRT installation
225
+ const hasExistingInstall = await fs.pathExists(drtDir);
226
+
227
+ // Track action type (only set if there's an existing installation)
228
+ let actionType;
229
+
230
+ // Only show action menu if there's an existing installation
231
+ if (hasExistingInstall) {
232
+ // Get version information
233
+ const { existingInstall, drtDir } = await this.getExistingInstallation(confirmedDirectory);
234
+
235
+ // Build menu choices dynamically
236
+ const choices = [];
237
+
238
+ // Always show Quick Update first (allows refreshing installation even on same version)
239
+ if (existingInstall.installed) {
240
+ choices.push({
241
+ name: 'Quick Update',
242
+ value: 'quick-update',
243
+ });
244
+ }
245
+
246
+ // Common actions
247
+ choices.push({ name: 'Modify DRT Installation', value: 'update' });
248
+
249
+ // Check if action is provided via command-line
250
+ if (options.action) {
251
+ const validActions = choices.map((c) => c.value);
252
+ if (!validActions.includes(options.action)) {
253
+ throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`);
254
+ }
255
+ actionType = options.action;
256
+ await prompts.log.info(`Using action from command-line: ${actionType}`);
257
+ } else if (options.yes) {
258
+ // Default to quick-update if available, unless flags that require the
259
+ // full update path are present (e.g. --custom-source which re-clones
260
+ // modules at a new version — quick-update skips that entirely).
261
+ if (choices.length === 0) {
262
+ throw new Error('No valid actions available for this installation');
263
+ }
264
+ const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
265
+ const needsFullUpdate = !!options.customSource;
266
+ actionType = hasQuickUpdate && !needsFullUpdate ? 'quick-update' : (choices.find((c) => c.value === 'update') || choices[0]).value;
267
+ await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
268
+ } else {
269
+ actionType = await prompts.select({
270
+ message: 'How would you like to proceed?',
271
+ choices: choices,
272
+ default: choices[0].value,
273
+ });
274
+ }
275
+
276
+ // Handle quick update separately
277
+ if (actionType === 'quick-update') {
278
+ return {
279
+ actionType: 'quick-update',
280
+ directory: confirmedDirectory,
281
+ skipPrompts: options.yes || false,
282
+ };
283
+ }
284
+
285
+ // If actionType === 'update', handle it with the new flow
286
+ // Return early with modify configuration
287
+ if (actionType === 'update') {
288
+ // Get existing installation info
289
+ const { installedModuleIds, installedModuleVersions } = await this.getExistingInstallation(confirmedDirectory);
290
+
291
+ await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`);
292
+
293
+ // Unified module selection - all modules in one grouped multiselect
294
+ let selectedModules;
295
+ if (options.modules) {
296
+ // Use modules from command-line
297
+ selectedModules = options.modules
298
+ .split(',')
299
+ .map((m) => m.trim())
300
+ .filter(Boolean);
301
+ await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
302
+ } else if (options.customSource && !options.yes) {
303
+ // Custom source without --modules or --yes: start with empty list
304
+ // (only custom source modules + core will be installed).
305
+ // When --yes is also set, fall through to the --yes branch so all
306
+ // installed modules are included alongside the custom source modules.
307
+ selectedModules = [];
308
+ } else if (options.yes) {
309
+ selectedModules = await this.getDefaultModules(installedModuleIds);
310
+ await prompts.log.info(
311
+ `Non-interactive mode (--yes): using default modules (installed + defaults): ${selectedModules.join(', ')}`,
312
+ );
313
+ } else {
314
+ selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions);
315
+ }
316
+
317
+ // Resolve custom sources from --custom-source flag
318
+ if (options.customSource) {
319
+ const customCodes = await this._resolveCustomSourcesCli(options.customSource);
320
+ for (const code of customCodes) {
321
+ if (!selectedModules.includes(code)) selectedModules.push(code);
322
+ }
323
+ }
324
+
325
+ // Ensure core is in the modules list
326
+ if (!selectedModules.includes('core')) {
327
+ selectedModules.unshift('core');
328
+ }
329
+
330
+ const retainedModuleResult = await this._retainUnavailableInstalledModules(selectedModules, installedModuleIds, drtDir, {
331
+ preserveUnselected: options.yes && !options.modules,
332
+ });
333
+ selectedModules = retainedModuleResult.selectedModules;
334
+ const preservedModules = retainedModuleResult.preserveModules;
335
+
336
+ if (preservedModules.length > 0) {
337
+ await prompts.log.warn(
338
+ `Retaining ${preservedModules.length} installed module(s) with no available source: ${preservedModules.join(', ')}`,
339
+ );
340
+ }
341
+
342
+ // For existing installs, resolve per-module update decisions BEFORE
343
+ // we clone anything. Reads the existing manifest's recorded channel
344
+ // per module and prompts the user on available upgrades (patch/minor
345
+ // default Y, major default N). Legacy entries with no channel are
346
+ // migrated here too. Mutates channelOptions.pins to lock rejections.
347
+ await this._resolveUpdateChannels({
348
+ drtDir,
349
+ selectedModules,
350
+ channelOptions,
351
+ yes: options.yes || false,
352
+ });
353
+
354
+ // Get tool selection
355
+ const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
356
+
357
+ const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
358
+ ...options,
359
+ channelOptions,
360
+ });
361
+
362
+ // Warn about --pin/--next flags that refer to modules the user didn't
363
+ // select, or that target bundled modules (core/bmm) where channel
364
+ // flags don't apply.
365
+ {
366
+ const bundledCodes = await this._bundledModuleCodes();
367
+ for (const warning of [
368
+ ...orphanPinWarnings(channelOptions, selectedModules),
369
+ ...bundledTargetWarnings(channelOptions, bundledCodes),
370
+ ]) {
371
+ await prompts.log.warn(warning);
372
+ }
373
+ }
374
+
375
+ return {
376
+ actionType: 'update',
377
+ directory: confirmedDirectory,
378
+ modules: selectedModules,
379
+ ides: toolSelection.ides,
380
+ skipIde: toolSelection.skipIde,
381
+ coreConfig: moduleConfigs.core || {},
382
+ moduleConfigs: moduleConfigs,
383
+ setOverrides,
384
+ skipPrompts: options.yes || false,
385
+ channelOptions,
386
+ _preserveModules: preservedModules,
387
+ };
388
+ }
389
+ }
390
+
391
+ // This section is only for new installations (update returns early above)
392
+ const { installedModuleIds, installedModuleVersions } = await this.getExistingInstallation(confirmedDirectory);
393
+
394
+ // Unified module selection - all modules in one grouped multiselect
395
+ let selectedModules;
396
+ if (options.modules) {
397
+ // Use modules from command-line
398
+ selectedModules = options.modules
399
+ .split(',')
400
+ .map((m) => m.trim())
401
+ .filter(Boolean);
402
+ await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
403
+ } else if (options.customSource) {
404
+ // Custom source without --modules: start with empty list (core added below)
405
+ selectedModules = [];
406
+ } else if (options.yes) {
407
+ // Use default modules when --yes flag is set
408
+ selectedModules = await this.getDefaultModules(installedModuleIds);
409
+ await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`);
410
+ } else {
411
+ selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions);
412
+ }
413
+
414
+ // Resolve custom sources from --custom-source flag
415
+ if (options.customSource) {
416
+ const customCodes = await this._resolveCustomSourcesCli(options.customSource);
417
+ for (const code of customCodes) {
418
+ if (!selectedModules.includes(code)) selectedModules.push(code);
419
+ }
420
+ }
421
+
422
+ // Ensure core is in the modules list
423
+ if (!selectedModules.includes('core')) {
424
+ selectedModules.unshift('core');
425
+ }
426
+
427
+ // Interactive channel gate: "Ready to install (all stable)? [Y/n]"
428
+ // Only shown for fresh installs with no channel flags and an external module
429
+ // selected. Skipped for prerelease launches because channelOptions.global
430
+ // was already seeded to 'next' upstream. Non-interactive installs skip this
431
+ // and fall through to the registry default (stable) or whatever flags were
432
+ // supplied.
433
+ await this._interactiveChannelGate({ options, channelOptions, selectedModules });
434
+
435
+ let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
436
+ const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
437
+ ...options,
438
+ channelOptions,
439
+ });
440
+
441
+ // Warn about --pin/--next flags that refer to modules the user didn't
442
+ // select, or that target bundled modules (core/bmm) where channel
443
+ // flags don't apply.
444
+ {
445
+ const bundledCodes = await this._bundledModuleCodes();
446
+ for (const warning of [
447
+ ...orphanPinWarnings(channelOptions, selectedModules),
448
+ ...bundledTargetWarnings(channelOptions, bundledCodes),
449
+ ]) {
450
+ await prompts.log.warn(warning);
451
+ }
452
+ }
453
+
454
+ return {
455
+ actionType: 'install',
456
+ directory: confirmedDirectory,
457
+ modules: selectedModules,
458
+ ides: toolSelection.ides,
459
+ skipIde: toolSelection.skipIde,
460
+ coreConfig: moduleConfigs.core || {},
461
+ moduleConfigs: moduleConfigs,
462
+ setOverrides,
463
+ skipPrompts: options.yes || false,
464
+ channelOptions,
465
+ };
466
+ }
467
+
468
+ /**
469
+ * Prompt for tool/IDE selection (called after module configuration)
470
+ * Uses a split prompt approach:
471
+ * 1. Recommended tools - standard multiselect for preferred tools
472
+ * 2. Additional tools - autocompleteMultiselect with search capability
473
+ * @param {string} projectDir - Project directory to check for existing IDEs
474
+ * @param {Object} options - Command-line options
475
+ * @returns {Object} Tool configuration
476
+ */
477
+ _parseToolsFlag(toolsArg, allKnownValues) {
478
+ const selectedIdes = toolsArg
479
+ .split(',')
480
+ .map((t) => t.trim())
481
+ .filter(Boolean);
482
+
483
+ if (selectedIdes.length === 0) {
484
+ const err = new Error(
485
+ '--tools was passed empty. Provide at least one tool ID (e.g. --tools claude-code) or run with --list-tools to see valid IDs.',
486
+ );
487
+ err.expected = true;
488
+ throw err;
489
+ }
490
+
491
+ const unknown = selectedIdes.filter((id) => !allKnownValues.has(id));
492
+ if (unknown.length > 0) {
493
+ const err = new Error(
494
+ [
495
+ `Unknown tool ID${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`,
496
+ '',
497
+ 'Run with --list-tools to see all valid IDs.',
498
+ 'Common: claude-code, cursor, copilot, windsurf, cline',
499
+ ].join('\n'),
500
+ );
501
+ err.expected = true;
502
+ throw err;
503
+ }
504
+
505
+ return selectedIdes;
506
+ }
507
+
508
+ async promptToolSelection(projectDir, options = {}) {
509
+ const { ExistingInstall } = require('./core/existing-install');
510
+ const { Installer } = require('./core/installer');
511
+ const installer = new Installer();
512
+ const { drtDir } = await installer.findDrtDir(projectDir || process.cwd());
513
+ const existingInstall = await ExistingInstall.detect(drtDir);
514
+ const configuredIdes = existingInstall.ides;
515
+
516
+ // Get IDE manager to fetch available IDEs dynamically
517
+ const { IdeManager } = require('./ide/manager');
518
+ const ideManager = new IdeManager();
519
+ await ideManager.ensureInitialized(); // IMPORTANT: Must initialize before getting IDEs
520
+
521
+ const preferredIdes = ideManager.getPreferredIdes();
522
+ const otherIdes = ideManager.getOtherIdes();
523
+
524
+ // Determine which configured IDEs are in "preferred" vs "other" categories
525
+ const configuredPreferred = configuredIdes.filter((id) => preferredIdes.some((ide) => ide.value === id));
526
+ const configuredOther = configuredIdes.filter((id) => otherIdes.some((ide) => ide.value === id));
527
+
528
+ // Warn about previously configured tools that are no longer available
529
+ const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value));
530
+ const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id));
531
+ if (unknownTools.length > 0) {
532
+ await prompts.log.warn(`Previously configured tools are no longer available: ${unknownTools.join(', ')}`);
533
+ }
534
+
535
+ // ─────────────────────────────────────────────────────────────────────────────
536
+ // UPGRADE PATH: If tools already configured, show all tools with configured at top
537
+ // ─────────────────────────────────────────────────────────────────────────────
538
+ if (configuredIdes.length > 0) {
539
+ const allTools = [...preferredIdes, ...otherIdes];
540
+
541
+ // Non-interactive: handle --tools and --yes flags before interactive prompt
542
+ // Use !== undefined so an explicit --tools "" falls through to _parseToolsFlag and
543
+ // gets a specific "passed empty" error instead of being silently ignored.
544
+ if (options.tools !== undefined) {
545
+ const selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
546
+ await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
547
+ await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
548
+ return { ides: selectedIdes, skipIde: false };
549
+ }
550
+
551
+ if (options.yes) {
552
+ await prompts.log.info(`Non-interactive mode (--yes): keeping configured tools: ${configuredIdes.join(', ')}`);
553
+ await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
554
+ return { ides: configuredIdes, skipIde: false };
555
+ }
556
+
557
+ // Sort: configured tools first, then preferred, then others
558
+ const sortedTools = [
559
+ ...allTools.filter((ide) => configuredIdes.includes(ide.value)),
560
+ ...allTools.filter((ide) => !configuredIdes.includes(ide.value)),
561
+ ];
562
+
563
+ const upgradeOptions = sortedTools.map((ide) => {
564
+ const isConfigured = configuredIdes.includes(ide.value);
565
+ const isPreferred = preferredIdes.some((p) => p.value === ide.value);
566
+ let label = ide.name;
567
+ if (isPreferred) label += ' ⭐';
568
+ if (isConfigured) label += ' ✅';
569
+ return { label, value: ide.value };
570
+ });
571
+
572
+ // Sort initialValues to match display order
573
+ const sortedInitialValues = sortedTools.filter((ide) => configuredIdes.includes(ide.value)).map((ide) => ide.value);
574
+
575
+ const upgradeSelected = await prompts.autocompleteMultiselect({
576
+ message: 'Integrate with',
577
+ options: upgradeOptions,
578
+ initialValues: sortedInitialValues,
579
+ required: false,
580
+ maxItems: 8,
581
+ });
582
+
583
+ const selectedIdes = upgradeSelected || [];
584
+
585
+ if (selectedIdes.length === 0) {
586
+ const confirmNoTools = await prompts.confirm({
587
+ message: 'No tools selected. Continue without installing any tools?',
588
+ default: false,
589
+ });
590
+
591
+ if (!confirmNoTools) {
592
+ return this.promptToolSelection(projectDir, options);
593
+ }
594
+
595
+ return { ides: [], skipIde: true };
596
+ }
597
+
598
+ // Display selected tools
599
+ await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
600
+
601
+ return { ides: selectedIdes, skipIde: false };
602
+ }
603
+
604
+ // ─────────────────────────────────────────────────────────────────────────────
605
+ // NEW INSTALL: Show all tools with search
606
+ // ─────────────────────────────────────────────────────────────────────────────
607
+ const allTools = [...preferredIdes, ...otherIdes];
608
+
609
+ const allToolOptions = allTools.map((ide) => {
610
+ const isPreferred = preferredIdes.some((p) => p.value === ide.value);
611
+ let label = ide.name;
612
+ if (isPreferred) label += ' ⭐';
613
+ return {
614
+ label,
615
+ value: ide.value,
616
+ };
617
+ });
618
+
619
+ let selectedIdes = [];
620
+
621
+ // Check if tools are provided via command-line.
622
+ // Use !== undefined so an explicit --tools "" still hits _parseToolsFlag's empty-value error.
623
+ if (options.tools !== undefined) {
624
+ selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
625
+ await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
626
+ await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
627
+ return { ides: selectedIdes, skipIde: false };
628
+ } else if (options.yes) {
629
+ // If --yes flag is set, skip tool prompt and use previously configured tools or empty
630
+ if (configuredIdes.length > 0) {
631
+ await prompts.log.info(`Using previously configured tools (--yes flag): ${configuredIdes.join(', ')}`);
632
+ await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
633
+ return { ides: configuredIdes, skipIde: false };
634
+ } else {
635
+ const err = new Error(
636
+ [
637
+ '--tools is required for non-interactive install (--yes / -y) when no tools are previously configured.',
638
+ '',
639
+ 'Common: claude-code, cursor, copilot, windsurf, cline',
640
+ 'See all supported tools: drt-method install --list-tools',
641
+ '',
642
+ 'Example: drt-method install --modules drt --tools claude-code -y',
643
+ ].join('\n'),
644
+ );
645
+ err.expected = true;
646
+ throw err;
647
+ }
648
+ }
649
+
650
+ // Interactive mode
651
+ const interactiveSelectedIdes = await prompts.autocompleteMultiselect({
652
+ message: 'Integrate with:',
653
+ options: allToolOptions,
654
+ initialValues: configuredIdes.length > 0 ? configuredIdes : undefined,
655
+ required: false,
656
+ maxItems: 8,
657
+ });
658
+
659
+ selectedIdes = interactiveSelectedIdes || [];
660
+
661
+ // ─────────────────────────────────────────────────────────────────────────────
662
+ // STEP 3: Confirm if no tools selected
663
+ // ─────────────────────────────────────────────────────────────────────────────
664
+ if (selectedIdes.length === 0) {
665
+ const confirmNoTools = await prompts.confirm({
666
+ message: 'No tools selected. Continue without installing any tools?',
667
+ default: false,
668
+ });
669
+
670
+ if (!confirmNoTools) {
671
+ // User wants to select tools - recurse
672
+ return this.promptToolSelection(projectDir, options);
673
+ }
674
+
675
+ return {
676
+ ides: [],
677
+ skipIde: true,
678
+ };
679
+ }
680
+
681
+ // Display selected tools
682
+ await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
683
+
684
+ return {
685
+ ides: selectedIdes,
686
+ skipIde: selectedIdes.length === 0,
687
+ };
688
+ }
689
+
690
+ /**
691
+ * Prompt for update configuration
692
+ * @returns {Object} Update configuration
693
+ */
694
+ async promptUpdate() {
695
+ const backupFirst = await prompts.confirm({
696
+ message: 'Create backup before updating?',
697
+ default: true,
698
+ });
699
+
700
+ const preserveCustomizations = await prompts.confirm({
701
+ message: 'Preserve local customizations?',
702
+ default: true,
703
+ });
704
+
705
+ return { backupFirst, preserveCustomizations };
706
+ }
707
+
708
+ /**
709
+ * Confirm action
710
+ * @param {string} message - Confirmation message
711
+ * @param {boolean} defaultValue - Default value
712
+ * @returns {boolean} User confirmation
713
+ */
714
+ async confirm(message, defaultValue = false) {
715
+ return await prompts.confirm({
716
+ message,
717
+ default: defaultValue,
718
+ });
719
+ }
720
+
721
+ /**
722
+ * Get confirmed directory from user
723
+ * @returns {string} Confirmed directory path
724
+ */
725
+ async getConfirmedDirectory() {
726
+ let confirmedDirectory = null;
727
+ while (!confirmedDirectory) {
728
+ const directoryAnswer = await this.promptForDirectory();
729
+ await this.displayDirectoryInfo(directoryAnswer.directory);
730
+
731
+ if (await this.confirmDirectory(directoryAnswer.directory)) {
732
+ confirmedDirectory = directoryAnswer.directory;
733
+ }
734
+ }
735
+ return confirmedDirectory;
736
+ }
737
+
738
+ /**
739
+ * Get existing installation info and installed modules
740
+ * @param {string} directory - Installation directory
741
+ * @returns {Object} Object with existingInstall, installedModuleIds, installedModuleVersions, and drtDir
742
+ */
743
+ async getExistingInstallation(directory) {
744
+ const { ExistingInstall } = require('./core/existing-install');
745
+ const { Installer } = require('./core/installer');
746
+ const installer = new Installer();
747
+ const { drtDir } = await installer.findDrtDir(directory);
748
+ const existingInstall = await ExistingInstall.detect(drtDir);
749
+ const installedModuleIds = new Set(existingInstall.moduleIds);
750
+ const installedModuleVersions = new Map();
751
+ const manifestModules = await manifest.getAllModuleVersions(drtDir);
752
+
753
+ for (const module of manifestModules) {
754
+ if (module?.name && module.version) {
755
+ installedModuleVersions.set(module.name, module.version);
756
+ }
757
+ }
758
+
759
+ for (const module of existingInstall.modules) {
760
+ if (module?.id && module.version && module.version !== 'unknown' && !installedModuleVersions.has(module.id)) {
761
+ installedModuleVersions.set(module.id, module.version);
762
+ }
763
+ }
764
+
765
+ if (existingInstall.hasCore && existingInstall.version && !installedModuleVersions.has('core')) {
766
+ installedModuleVersions.set('core', existingInstall.version);
767
+ }
768
+
769
+ return { existingInstall, installedModuleIds, installedModuleVersions, drtDir };
770
+ }
771
+
772
+ /**
773
+ * Collect all module configurations (core + selected modules).
774
+ * All interactive prompting happens here in the UI layer.
775
+ * @param {string} directory - Installation directory
776
+ * @param {string[]} modules - Modules to configure (including 'core')
777
+ * @param {Object} options - Command-line options
778
+ * @returns {Object} Collected module configurations keyed by module name
779
+ */
780
+ async collectModuleConfigs(directory, modules, options = {}) {
781
+ const { OfficialModules } = require('./modules/official-modules');
782
+
783
+ // Parse --set up front purely to surface user-error before the install
784
+ // burns time on the network / filesystem. The actual application happens
785
+ // in installer.install() as a post-write TOML patch — see
786
+ // `tools/installer/set-overrides.js`. We also warn about overrides
787
+ // targeting modules the user didn't include, since those will silently
788
+ // miss the file the patch step looks for.
789
+ let setOverrides = {};
790
+ try {
791
+ setOverrides = parseSetEntries(options.set || []);
792
+ } catch (error) {
793
+ // install.js validated already; rethrow as-is for the user.
794
+ throw error;
795
+ }
796
+ // Drop overrides for modules that aren't in the install set so the
797
+ // post-install patch step doesn't create orphan sections in config.toml
798
+ // for modules that were never installed.
799
+ const selectedModuleSet = new Set(['core', ...modules]);
800
+ for (const moduleCode of Object.keys(setOverrides)) {
801
+ if (!selectedModuleSet.has(moduleCode)) {
802
+ await prompts.log.warn(
803
+ `--set ${moduleCode}.* — module '${moduleCode}' is not in the install set; values will be ignored. Add it to --modules to apply.`,
804
+ );
805
+ delete setOverrides[moduleCode];
806
+ }
807
+ }
808
+
809
+ const configCollector = new OfficialModules({ channelOptions: options.channelOptions });
810
+
811
+ // Seed core config from CLI options if provided
812
+ if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) {
813
+ const coreConfig = {};
814
+ if (options.userName) {
815
+ coreConfig.user_name = options.userName;
816
+ await prompts.log.info(`Using user name from command-line: ${options.userName}`);
817
+ }
818
+ if (options.communicationLanguage) {
819
+ coreConfig.communication_language = options.communicationLanguage;
820
+ await prompts.log.info(`Using communication language from command-line: ${options.communicationLanguage}`);
821
+ }
822
+ if (options.documentOutputLanguage) {
823
+ coreConfig.document_output_language = options.documentOutputLanguage;
824
+ await prompts.log.info(`Using document output language from command-line: ${options.documentOutputLanguage}`);
825
+ }
826
+ if (options.outputFolder) {
827
+ coreConfig.output_folder = options.outputFolder;
828
+ await prompts.log.info(`Using output folder from command-line: ${options.outputFolder}`);
829
+ }
830
+
831
+ // Load existing config to merge with provided options
832
+ await configCollector.loadExistingConfig(directory);
833
+ const existingConfig = configCollector.collectedConfig.core || {};
834
+ configCollector.collectedConfig.core = { ...existingConfig, ...coreConfig };
835
+
836
+ // If not all options are provided, collect the missing ones interactively (unless --yes flag)
837
+ if (
838
+ !options.yes &&
839
+ (!options.userName || !options.communicationLanguage || !options.documentOutputLanguage || !options.outputFolder)
840
+ ) {
841
+ await configCollector.collectModuleConfig('core', directory, false, true);
842
+ }
843
+ } else if (options.yes) {
844
+ // Use all defaults when --yes flag is set
845
+ await configCollector.loadExistingConfig(directory);
846
+ const existingConfig = configCollector.collectedConfig.core || {};
847
+
848
+ if (Object.keys(existingConfig).length === 0) {
849
+ let safeUsername;
850
+ try {
851
+ safeUsername = os.userInfo().username;
852
+ } catch {
853
+ safeUsername = process.env.USER || process.env.USERNAME || 'User';
854
+ }
855
+ const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1);
856
+ configCollector.collectedConfig.core = {
857
+ user_name: defaultUsername,
858
+ // {directory_name} default per src/core-skills/module.yaml — matches what the
859
+ // interactive flow resolves via buildQuestion()'s {directory_name} placeholder.
860
+ project_name: path.basename(directory),
861
+ communication_language: 'English',
862
+ document_output_language: 'English',
863
+ output_folder: '_drt-output',
864
+ };
865
+ await prompts.log.info('Using default configuration (--yes flag)');
866
+ }
867
+ }
868
+
869
+ // Collect all module configs — core is skipped if already seeded above
870
+ await configCollector.collectAllConfigurations(modules, directory, {
871
+ skipPrompts: options.yes || false,
872
+ });
873
+
874
+ return { moduleConfigs: configCollector.collectedConfig, setOverrides };
875
+ }
876
+
877
+ /**
878
+ * Select all modules across three tiers: official, community, and custom URL.
879
+ * @param {Set} installedModuleIds - Currently installed module IDs
880
+ * @param {Map<string, string>} installedModuleVersions - Installed module versions from the local manifest
881
+ * @param {Object|null} channelOptions - Parsed installer channel options
882
+ * @returns {Array} Selected module codes (excluding core)
883
+ */
884
+ async selectAllModules(installedModuleIds = new Set(), installedModuleVersions = new Map(), channelOptions = null) {
885
+ // Phase 1: Official modules
886
+ const officialSelected = await this._selectOfficialModules(installedModuleIds, installedModuleVersions, channelOptions);
887
+
888
+ // Identify installed modules that aren't official (previously installed
889
+ // community modules or custom-source modules). Preserve them on update;
890
+ // they can be managed via --custom-source, uninstall, or a dedicated installer.
891
+ const externalManager = new ExternalModuleManager();
892
+ const registryModules = await externalManager.listAvailable();
893
+ const officialRegistryCodes = new Set(['core', 'drt', ...registryModules.map((m) => m.code)]);
894
+ const installedNonOfficial = [...installedModuleIds].filter((id) => !officialRegistryCodes.has(id));
895
+
896
+ // Phase 2: Custom URL modules
897
+ const customSelected = await this._addCustomUrlModules(installedModuleIds);
898
+
899
+ const allSelected = new Set([...officialSelected, ...customSelected, ...installedNonOfficial]);
900
+
901
+ return [...allSelected];
902
+ }
903
+
904
+ /**
905
+ * Select official modules using autocompleteMultiselect.
906
+ * Extracted from the original selectAllModules - unchanged behavior.
907
+ * @param {Set} installedModuleIds - Currently installed module IDs
908
+ * @param {Map<string, string>} installedModuleVersions - Installed module versions from the local manifest
909
+ * @param {Object|null} channelOptions - Parsed installer channel options
910
+ * @returns {Array} Selected official module codes
911
+ */
912
+ async _selectOfficialModules(installedModuleIds = new Set(), installedModuleVersions = new Map(), channelOptions = null) {
913
+ // Built-in modules (core, bmm) come from local source, not the registry
914
+ const { OfficialModules } = require('./modules/official-modules');
915
+ const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
916
+
917
+ // External modules come from the registry (with fallback)
918
+ const externalManager = new ExternalModuleManager();
919
+ const registryModules = await externalManager.listAvailable();
920
+
921
+ const allOptions = [];
922
+ const initialValues = [];
923
+ const lockedValues = ['core'];
924
+
925
+ const buildModuleEntry = async (code, name, description, isDefault, repoUrl = null, registryDefault = null) => {
926
+ const isInstalled = installedModuleIds.has(code);
927
+ const installedVersion = installedModuleVersions.get(code) || '';
928
+ const versionState = await getModuleVersion(code, { repoUrl, registryDefault, channelOptions });
929
+ const label = buildModuleLabel(name, versionState.version, installedVersion);
930
+ return {
931
+ label,
932
+ value: code,
933
+ hint: description,
934
+ selected: isInstalled || isDefault,
935
+ lookupAttempted: versionState.lookupAttempted,
936
+ lookupSucceeded: versionState.lookupSucceeded,
937
+ };
938
+ };
939
+
940
+ // Add built-in modules first (always available regardless of network)
941
+ const builtInCodes = new Set();
942
+ for (const mod of builtInModules) {
943
+ const code = mod.id;
944
+ builtInCodes.add(code);
945
+ const entry = await buildModuleEntry(code, mod.name, mod.description, mod.defaultSelected);
946
+ allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
947
+ if (entry.selected) {
948
+ initialValues.push(code);
949
+ }
950
+ }
951
+
952
+ // Add external registry modules (skip built-in duplicates and deprecated
953
+ // modules that are not already installed — deprecated modules stay visible
954
+ // only so existing users can continue to manage them).
955
+ const externalRegistryModules = registryModules.filter(
956
+ (mod) => !mod.builtIn && !builtInCodes.has(mod.code) && (!mod.deprecated || installedModuleIds.has(mod.code)),
957
+ );
958
+ let externalRegistryEntries = [];
959
+ if (externalRegistryModules.length > 0) {
960
+ const spinner = await prompts.spinner();
961
+ spinner.start('Checking latest module versions...');
962
+
963
+ externalRegistryEntries = await Promise.all(
964
+ externalRegistryModules.map(async (mod) => {
965
+ const entry = await buildModuleEntry(
966
+ mod.code,
967
+ mod.name,
968
+ mod.description,
969
+ mod.defaultSelected,
970
+ mod.url || null,
971
+ mod.defaultChannel || null,
972
+ );
973
+ if (mod.deprecated && mod.deprecationMessage) {
974
+ entry.hint = entry.hint ? `${entry.hint} — ${mod.deprecationMessage}` : mod.deprecationMessage;
975
+ }
976
+ return { code: mod.code, entry };
977
+ }),
978
+ );
979
+
980
+ spinner.stop('Checked latest module versions.');
981
+
982
+ const attemptedLookups = externalRegistryEntries.filter(({ entry }) => entry.lookupAttempted).length;
983
+ const successfulLookups = externalRegistryEntries.filter(({ entry }) => entry.lookupSucceeded).length;
984
+ if (attemptedLookups > 0 && successfulLookups === 0) {
985
+ await prompts.log.warn('Could not check latest module versions; showing cached/local versions.');
986
+ }
987
+ }
988
+ for (const { code, entry } of externalRegistryEntries) {
989
+ allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
990
+ if (entry.selected) {
991
+ initialValues.push(code);
992
+ }
993
+ }
994
+
995
+ const selected = await prompts.autocompleteMultiselect({
996
+ message: 'Select official modules to install:',
997
+ options: allOptions,
998
+ initialValues: initialValues.length > 0 ? initialValues : undefined,
999
+ lockedValues,
1000
+ required: true,
1001
+ maxItems: allOptions.length,
1002
+ });
1003
+
1004
+ const result = selected ? [...selected] : [];
1005
+
1006
+ if (result.length > 0) {
1007
+ const moduleLines = result.map((moduleId) => {
1008
+ const opt = allOptions.find((o) => o.value === moduleId);
1009
+ return ` \u2022 ${opt?.label || moduleId}`;
1010
+ });
1011
+ await prompts.log.message('Selected official modules:\n' + moduleLines.join('\n'));
1012
+ }
1013
+
1014
+ return result;
1015
+ }
1016
+
1017
+ /**
1018
+ * Prompt user to install modules from custom sources (Git URLs or local paths).
1019
+ * @param {Set} installedModuleIds - Currently installed module IDs
1020
+ * @returns {Array} Selected custom module code strings
1021
+ */
1022
+ async _addCustomUrlModules(installedModuleIds = new Set()) {
1023
+ const addCustom = await prompts.confirm({
1024
+ message: 'Do you want to install custom or community modules (Git URL or local path)?',
1025
+ default: false,
1026
+ });
1027
+ if (!addCustom) return [];
1028
+
1029
+ const { CustomModuleManager } = require('./modules/custom-module-manager');
1030
+ const customMgr = new CustomModuleManager();
1031
+ const selectedModules = [];
1032
+
1033
+ let addMore = true;
1034
+ while (addMore) {
1035
+ const sourceInput = await prompts.text({
1036
+ message: 'Git URL or local path:',
1037
+ placeholder: 'https://github.com/owner/repo or /path/to/module',
1038
+ validate: (input) => {
1039
+ if (!input || input.trim() === '') return 'Source is required';
1040
+ const result = customMgr.parseSource(input.trim());
1041
+ return result.isValid ? undefined : result.error;
1042
+ },
1043
+ });
1044
+
1045
+ const s = await prompts.spinner();
1046
+ s.start('Resolving source...');
1047
+
1048
+ let sourceResult;
1049
+ try {
1050
+ sourceResult = await customMgr.resolveSource(sourceInput.trim(), { skipInstall: true, silent: true });
1051
+ s.stop(sourceResult.parsed.type === 'local' ? 'Local source resolved' : 'Repository cloned');
1052
+ } catch (error) {
1053
+ s.error('Failed to resolve source');
1054
+ await prompts.log.error(` ${error.message}`);
1055
+ addMore = await prompts.confirm({ message: 'Try another source?', default: false });
1056
+ continue;
1057
+ }
1058
+
1059
+ if (sourceResult.parsed.type === 'local') {
1060
+ await prompts.log.info('LOCAL MODULE: Pointing directly at local source (changes take effect on reinstall).');
1061
+ } else {
1062
+ await prompts.log.warn(
1063
+ 'UNVERIFIED MODULE: This module has not been reviewed by the DRT team.\n' + ' Only install modules from sources you trust.',
1064
+ );
1065
+ }
1066
+
1067
+ // Resolve plugins based on discovery mode vs direct mode
1068
+ s.start('Analyzing plugin structure...');
1069
+ const allResolved = [];
1070
+ const localPath = sourceResult.parsed.type === 'local' ? sourceResult.rootDir : null;
1071
+
1072
+ if (sourceResult.mode === 'discovery') {
1073
+ // Discovery mode: marketplace.json found, list available plugins
1074
+ let plugins;
1075
+ try {
1076
+ plugins = await customMgr.discoverModules(sourceResult.marketplace, sourceResult.sourceUrl);
1077
+ } catch (discoverError) {
1078
+ s.error('Failed to discover modules');
1079
+ await prompts.log.error(` ${discoverError.message}`);
1080
+ addMore = await prompts.confirm({ message: 'Try another source?', default: false });
1081
+ continue;
1082
+ }
1083
+
1084
+ const effectiveRepoPath = sourceResult.repoPath || sourceResult.rootDir;
1085
+ for (const plugin of plugins) {
1086
+ try {
1087
+ const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath);
1088
+ if (resolved.length > 0) {
1089
+ allResolved.push(...resolved);
1090
+ } else {
1091
+ // No skills array or empty - use plugin metadata as-is (legacy)
1092
+ allResolved.push({
1093
+ code: plugin.code,
1094
+ name: plugin.displayName || plugin.name,
1095
+ version: plugin.version,
1096
+ description: plugin.description,
1097
+ strategy: 0,
1098
+ pluginName: plugin.name,
1099
+ skillPaths: [],
1100
+ });
1101
+ }
1102
+ } catch (resolveError) {
1103
+ await prompts.log.warn(` Could not resolve ${plugin.name}: ${resolveError.message}`);
1104
+ }
1105
+ }
1106
+ } else {
1107
+ // Direct mode: no marketplace.json, scan directory for skills and resolve
1108
+ const directPlugin = {
1109
+ name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir),
1110
+ source: '.',
1111
+ skills: [],
1112
+ };
1113
+
1114
+ // Scan for SKILL.md directories to populate skills array
1115
+ try {
1116
+ const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true });
1117
+ for (const entry of entries) {
1118
+ if (entry.isDirectory()) {
1119
+ const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md');
1120
+ if (await fs.pathExists(skillMd)) {
1121
+ directPlugin.skills.push(entry.name);
1122
+ }
1123
+ }
1124
+ }
1125
+ } catch (scanError) {
1126
+ s.error('Failed to scan directory');
1127
+ await prompts.log.error(` ${scanError.message}`);
1128
+ addMore = await prompts.confirm({ message: 'Try another source?', default: false });
1129
+ continue;
1130
+ }
1131
+
1132
+ if (directPlugin.skills.length > 0) {
1133
+ try {
1134
+ const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath);
1135
+ allResolved.push(...resolved);
1136
+ } catch (resolveError) {
1137
+ await prompts.log.warn(` Could not resolve: ${resolveError.message}`);
1138
+ }
1139
+ }
1140
+ }
1141
+ s.stop(`Found ${allResolved.length} installable module${allResolved.length === 1 ? '' : 's'}`);
1142
+
1143
+ if (allResolved.length === 0) {
1144
+ await prompts.log.warn('No installable modules found in this source.');
1145
+ addMore = await prompts.confirm({ message: 'Try another source?', default: false });
1146
+ continue;
1147
+ }
1148
+
1149
+ // Build multiselect choices
1150
+ // Already-installed modules are pre-checked (update). New modules are unchecked (opt-in).
1151
+ // Unchecking an installed module means "skip update" - removal is handled elsewhere.
1152
+ const choices = allResolved.map((mod) => {
1153
+ const versionStr = mod.version ? ` v${mod.version}` : '';
1154
+ const skillCount = mod.skillPaths ? mod.skillPaths.length : 0;
1155
+ const skillStr = skillCount > 0 ? ` (${skillCount} skill${skillCount === 1 ? '' : 's'})` : '';
1156
+ const alreadyInstalled = installedModuleIds.has(mod.code);
1157
+ const hint = alreadyInstalled ? 'update' : undefined;
1158
+
1159
+ return {
1160
+ name: `${mod.name}${versionStr}${skillStr}`,
1161
+ value: mod.code,
1162
+ hint,
1163
+ checked: alreadyInstalled,
1164
+ };
1165
+ });
1166
+
1167
+ // Show descriptions before the multiselect
1168
+ for (const mod of allResolved) {
1169
+ const versionStr = mod.version ? ` v${mod.version}` : '';
1170
+ await prompts.log.info(` ${mod.name}${versionStr}\n ${mod.description}`);
1171
+ }
1172
+
1173
+ const selected = await prompts.multiselect({
1174
+ message: 'Select modules to install:',
1175
+ choices,
1176
+ required: false,
1177
+ });
1178
+
1179
+ if (selected && selected.length > 0) {
1180
+ for (const code of selected) {
1181
+ selectedModules.push(code);
1182
+ }
1183
+ }
1184
+
1185
+ addMore = await prompts.confirm({
1186
+ message: 'Add another custom source?',
1187
+ default: false,
1188
+ });
1189
+ }
1190
+
1191
+ if (selectedModules.length > 0) {
1192
+ await prompts.log.message('Selected custom modules:\n' + selectedModules.map((c) => ` \u2022 ${c}`).join('\n'));
1193
+ }
1194
+
1195
+ return selectedModules;
1196
+ }
1197
+
1198
+ /**
1199
+ * Resolve custom sources from --custom-source CLI flag (non-interactive).
1200
+ * Auto-selects all discovered modules from each source.
1201
+ * @param {string} sourcesArg - Comma-separated Git URLs or local paths
1202
+ * @returns {Array} Module codes from all resolved sources
1203
+ */
1204
+ async _resolveCustomSourcesCli(sourcesArg) {
1205
+ const { CustomModuleManager } = require('./modules/custom-module-manager');
1206
+ const customMgr = new CustomModuleManager();
1207
+ const allCodes = [];
1208
+
1209
+ const sources = sourcesArg
1210
+ .split(',')
1211
+ .map((s) => s.trim())
1212
+ .filter(Boolean);
1213
+
1214
+ for (const source of sources) {
1215
+ const s = await prompts.spinner();
1216
+ s.start(`Resolving ${source}...`);
1217
+
1218
+ let sourceResult;
1219
+ try {
1220
+ sourceResult = await customMgr.resolveSource(source, { skipInstall: true, silent: true });
1221
+ s.stop(sourceResult.parsed.type === 'local' ? 'Local source resolved' : 'Repository cloned');
1222
+ } catch (error) {
1223
+ s.error(`Failed to resolve ${source}`);
1224
+ await prompts.log.error(` ${error.message}`);
1225
+ continue;
1226
+ }
1227
+
1228
+ const s2 = await prompts.spinner();
1229
+ s2.start('Analyzing plugin structure...');
1230
+ const allResolved = [];
1231
+ const localPath = sourceResult.parsed.type === 'local' ? sourceResult.rootDir : null;
1232
+
1233
+ if (sourceResult.mode === 'discovery') {
1234
+ try {
1235
+ const plugins = await customMgr.discoverModules(sourceResult.marketplace, sourceResult.sourceUrl);
1236
+ const effectiveRepoPath = sourceResult.repoPath || sourceResult.rootDir;
1237
+ for (const plugin of plugins) {
1238
+ try {
1239
+ const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath);
1240
+ if (resolved.length > 0) {
1241
+ allResolved.push(...resolved);
1242
+ }
1243
+ } catch {
1244
+ // Skip unresolvable plugins
1245
+ }
1246
+ }
1247
+ } catch (discoverError) {
1248
+ s2.error('Failed to discover modules');
1249
+ await prompts.log.error(` ${discoverError.message}`);
1250
+ continue;
1251
+ }
1252
+ } else {
1253
+ // Direct mode: scan for SKILL.md directories
1254
+ const directPlugin = {
1255
+ name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir),
1256
+ source: '.',
1257
+ skills: [],
1258
+ };
1259
+ try {
1260
+ const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true });
1261
+ for (const entry of entries) {
1262
+ if (entry.isDirectory()) {
1263
+ const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md');
1264
+ if (await fs.pathExists(skillMd)) {
1265
+ directPlugin.skills.push(entry.name);
1266
+ }
1267
+ }
1268
+ }
1269
+ } catch {
1270
+ // Skip unreadable directories
1271
+ }
1272
+
1273
+ if (directPlugin.skills.length > 0) {
1274
+ try {
1275
+ const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath);
1276
+ allResolved.push(...resolved);
1277
+ } catch {
1278
+ // Skip unresolvable
1279
+ }
1280
+ }
1281
+ }
1282
+ s2.stop(`Found ${allResolved.length} module${allResolved.length === 1 ? '' : 's'}`);
1283
+
1284
+ for (const mod of allResolved) {
1285
+ allCodes.push(mod.code);
1286
+ const versionStr = mod.version ? ` v${mod.version}` : '';
1287
+ await prompts.log.info(` Custom module: ${mod.name}${versionStr}`);
1288
+ }
1289
+ }
1290
+
1291
+ return allCodes;
1292
+ }
1293
+
1294
+ /**
1295
+ * Get default modules for non-interactive mode
1296
+ * @param {Set} installedModuleIds - Already installed module IDs
1297
+ * @returns {Array} Default module codes
1298
+ */
1299
+ async getDefaultModules(installedModuleIds = new Set()) {
1300
+ // Built-in modules with default_selected come from local source
1301
+ const { OfficialModules } = require('./modules/official-modules');
1302
+ const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
1303
+
1304
+ const defaultModules = [];
1305
+ const seen = new Set();
1306
+
1307
+ for (const mod of builtInModules) {
1308
+ if (mod.defaultSelected || installedModuleIds.has(mod.id)) {
1309
+ defaultModules.push(mod.id);
1310
+ seen.add(mod.id);
1311
+ }
1312
+ }
1313
+
1314
+ // Add external registry defaults
1315
+ const externalManager = new ExternalModuleManager();
1316
+ const registryModules = await externalManager.listAvailable();
1317
+
1318
+ for (const mod of registryModules) {
1319
+ if (mod.builtIn || seen.has(mod.code)) continue;
1320
+ if (mod.defaultSelected || installedModuleIds.has(mod.code)) {
1321
+ defaultModules.push(mod.code);
1322
+ }
1323
+ }
1324
+
1325
+ // If no defaults found, use 'drt' as the fallback default
1326
+ if (defaultModules.length === 0) {
1327
+ defaultModules.push('drt');
1328
+ }
1329
+
1330
+ return defaultModules;
1331
+ }
1332
+
1333
+ /**
1334
+ * Prompt for directory selection
1335
+ * @returns {Object} Directory answer from prompt
1336
+ */
1337
+ async promptForDirectory() {
1338
+ // Use sync validation because @clack/prompts doesn't support async validate
1339
+ const directory = await prompts.directory({
1340
+ message: 'Installation directory:',
1341
+ default: process.cwd(),
1342
+ placeholder: process.cwd(),
1343
+ validate: (input) => this.validateDirectorySync(input),
1344
+ });
1345
+
1346
+ // Apply filter logic
1347
+ let filteredDir = directory;
1348
+ if (!filteredDir || filteredDir.trim() === '') {
1349
+ filteredDir = process.cwd();
1350
+ } else {
1351
+ filteredDir = this.expandUserPath(filteredDir);
1352
+ }
1353
+
1354
+ return { directory: filteredDir };
1355
+ }
1356
+
1357
+ /**
1358
+ * Display directory information
1359
+ * @param {string} directory - The directory path
1360
+ */
1361
+ async displayDirectoryInfo(directory) {
1362
+ await prompts.log.info(`Resolved installation path: ${directory}`);
1363
+
1364
+ const dirExists = await fs.pathExists(directory);
1365
+ if (dirExists) {
1366
+ // Show helpful context about the existing path
1367
+ const stats = await fs.stat(directory);
1368
+ if (stats.isDirectory()) {
1369
+ const files = await fs.readdir(directory);
1370
+ if (files.length > 0) {
1371
+ // Check for any drt installation (any folder with _config/manifest.yaml)
1372
+ const { Installer } = require('./core/installer');
1373
+ const installer = new Installer();
1374
+ const drtResult = await installer.findDrtDir(directory);
1375
+ const hasDrtInstall =
1376
+ (await fs.pathExists(drtResult.drtDir)) && (await fs.pathExists(path.join(drtResult.drtDir, '_config', 'manifest.yaml')));
1377
+
1378
+ const drtNote = hasDrtInstall ? ` including existing DRT installation (${path.basename(drtResult.drtDir)})` : '';
1379
+ await prompts.log.message(`Directory exists and contains ${files.length} item(s)${drtNote}`);
1380
+ } else {
1381
+ await prompts.log.message('Directory exists and is empty');
1382
+ }
1383
+ }
1384
+ }
1385
+ }
1386
+
1387
+ /**
1388
+ * Confirm directory selection
1389
+ * @param {string} directory - The directory path
1390
+ * @returns {boolean} Whether user confirmed
1391
+ */
1392
+ async confirmDirectory(directory) {
1393
+ const dirExists = await fs.pathExists(directory);
1394
+
1395
+ if (dirExists) {
1396
+ const proceed = await prompts.confirm({
1397
+ message: 'Install to this directory?',
1398
+ default: true,
1399
+ });
1400
+
1401
+ if (!proceed) {
1402
+ await prompts.log.warn("Let's try again with a different path.");
1403
+ }
1404
+
1405
+ return proceed;
1406
+ } else {
1407
+ // Ask for confirmation to create the directory
1408
+ const create = await prompts.confirm({
1409
+ message: `Create directory: ${directory}?`,
1410
+ default: false,
1411
+ });
1412
+
1413
+ if (!create) {
1414
+ await prompts.log.warn("Let's try again with a different path.");
1415
+ }
1416
+
1417
+ return create;
1418
+ }
1419
+ }
1420
+
1421
+ /**
1422
+ * Validate directory path for installation (sync version for clack prompts)
1423
+ * @param {string} input - User input path
1424
+ * @returns {string|undefined} Error message or undefined if valid
1425
+ */
1426
+ validateDirectorySync(input) {
1427
+ // Allow empty input to use the default
1428
+ if (!input || input.trim() === '') {
1429
+ return; // Empty means use default, undefined = valid for clack
1430
+ }
1431
+
1432
+ let expandedPath;
1433
+ try {
1434
+ expandedPath = this.expandUserPath(input.trim());
1435
+ } catch (error) {
1436
+ return error.message;
1437
+ }
1438
+
1439
+ // Check if the path exists
1440
+ const pathExists = fs.pathExistsSync(expandedPath);
1441
+
1442
+ if (!pathExists) {
1443
+ // Find the first existing parent directory
1444
+ const existingParent = this.findExistingParentSync(expandedPath);
1445
+
1446
+ if (!existingParent) {
1447
+ return 'Cannot create directory: no existing parent directory found';
1448
+ }
1449
+
1450
+ // Check if the existing parent is writable
1451
+ try {
1452
+ fs.accessSync(existingParent, fs.constants.W_OK);
1453
+ // Path doesn't exist but can be created - will prompt for confirmation later
1454
+ return;
1455
+ } catch {
1456
+ // Provide a detailed error message explaining both issues
1457
+ return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
1458
+ }
1459
+ }
1460
+
1461
+ // If it exists, validate it's a directory and writable
1462
+ const stat = fs.statSync(expandedPath);
1463
+ if (!stat.isDirectory()) {
1464
+ return `Path exists but is not a directory: ${expandedPath}`;
1465
+ }
1466
+
1467
+ // Check write permissions
1468
+ try {
1469
+ fs.accessSync(expandedPath, fs.constants.W_OK);
1470
+ } catch {
1471
+ return `Directory is not writable: ${expandedPath}`;
1472
+ }
1473
+
1474
+ return;
1475
+ }
1476
+
1477
+ /**
1478
+ * Validate directory path for installation (async version)
1479
+ * @param {string} input - User input path
1480
+ * @returns {string|true} Error message or true if valid
1481
+ */
1482
+ async validateDirectory(input) {
1483
+ // Allow empty input to use the default
1484
+ if (!input || input.trim() === '') {
1485
+ return true; // Empty means use default
1486
+ }
1487
+
1488
+ let expandedPath;
1489
+ try {
1490
+ expandedPath = this.expandUserPath(input.trim());
1491
+ } catch (error) {
1492
+ return error.message;
1493
+ }
1494
+
1495
+ // Check if the path exists
1496
+ const pathExists = await fs.pathExists(expandedPath);
1497
+
1498
+ if (!pathExists) {
1499
+ // Find the first existing parent directory
1500
+ const existingParent = await this.findExistingParent(expandedPath);
1501
+
1502
+ if (!existingParent) {
1503
+ return 'Cannot create directory: no existing parent directory found';
1504
+ }
1505
+
1506
+ // Check if the existing parent is writable
1507
+ try {
1508
+ await fs.access(existingParent, fs.constants.W_OK);
1509
+ // Path doesn't exist but can be created - will prompt for confirmation later
1510
+ return true;
1511
+ } catch {
1512
+ // Provide a detailed error message explaining both issues
1513
+ return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
1514
+ }
1515
+ }
1516
+
1517
+ // If it exists, validate it's a directory and writable
1518
+ const stat = await fs.stat(expandedPath);
1519
+ if (!stat.isDirectory()) {
1520
+ return `Path exists but is not a directory: ${expandedPath}`;
1521
+ }
1522
+
1523
+ // Check write permissions
1524
+ try {
1525
+ await fs.access(expandedPath, fs.constants.W_OK);
1526
+ } catch {
1527
+ return `Directory is not writable: ${expandedPath}`;
1528
+ }
1529
+
1530
+ return true;
1531
+ }
1532
+
1533
+ /**
1534
+ * Find the first existing parent directory (sync version)
1535
+ * @param {string} targetPath - The path to check
1536
+ * @returns {string|null} The first existing parent directory, or null if none found
1537
+ */
1538
+ findExistingParentSync(targetPath) {
1539
+ let currentPath = path.resolve(targetPath);
1540
+
1541
+ // Walk up the directory tree until we find an existing directory
1542
+ while (currentPath !== path.dirname(currentPath)) {
1543
+ // Stop at root
1544
+ const parent = path.dirname(currentPath);
1545
+ if (fs.pathExistsSync(parent)) {
1546
+ return parent;
1547
+ }
1548
+ currentPath = parent;
1549
+ }
1550
+
1551
+ return null; // No existing parent found (shouldn't happen in practice)
1552
+ }
1553
+
1554
+ /**
1555
+ * Find the first existing parent directory (async version)
1556
+ * @param {string} targetPath - The path to check
1557
+ * @returns {string|null} The first existing parent directory, or null if none found
1558
+ */
1559
+ async findExistingParent(targetPath) {
1560
+ let currentPath = path.resolve(targetPath);
1561
+
1562
+ // Walk up the directory tree until we find an existing directory
1563
+ while (currentPath !== path.dirname(currentPath)) {
1564
+ // Stop at root
1565
+ const parent = path.dirname(currentPath);
1566
+ if (await fs.pathExists(parent)) {
1567
+ return parent;
1568
+ }
1569
+ currentPath = parent;
1570
+ }
1571
+
1572
+ return null; // No existing parent found (shouldn't happen in practice)
1573
+ }
1574
+
1575
+ /**
1576
+ * Expands the user-provided path: handles ~ and resolves to absolute.
1577
+ * @param {string} inputPath - User input path.
1578
+ * @returns {string} Absolute expanded path.
1579
+ */
1580
+ expandUserPath(inputPath) {
1581
+ if (typeof inputPath !== 'string') {
1582
+ throw new TypeError('Path must be a string.');
1583
+ }
1584
+
1585
+ let expanded = inputPath.trim();
1586
+
1587
+ // Handle tilde expansion
1588
+ if (expanded.startsWith('~')) {
1589
+ if (expanded === '~') {
1590
+ expanded = os.homedir();
1591
+ } else if (expanded.startsWith('~' + path.sep)) {
1592
+ const pathAfterHome = expanded.slice(2); // Remove ~/ or ~\
1593
+ expanded = path.join(os.homedir(), pathAfterHome);
1594
+ } else {
1595
+ const restOfPath = expanded.slice(1);
1596
+ const separatorIndex = restOfPath.indexOf(path.sep);
1597
+ const username = separatorIndex === -1 ? restOfPath : restOfPath.slice(0, separatorIndex);
1598
+ if (username) {
1599
+ throw new Error(`Path expansion for ~${username} is not supported. Please use an absolute path or ~${path.sep}`);
1600
+ }
1601
+ }
1602
+ }
1603
+
1604
+ // Resolve to the absolute path relative to the current working directory
1605
+ return path.resolve(expanded);
1606
+ }
1607
+
1608
+ /**
1609
+ * Get configured IDEs from existing installation
1610
+ * @param {string} directory - Installation directory
1611
+ * @returns {Array} List of configured IDEs
1612
+ */
1613
+ async getConfiguredIdes(directory) {
1614
+ const { ExistingInstall } = require('./core/existing-install');
1615
+ const { Installer } = require('./core/installer');
1616
+ const installer = new Installer();
1617
+ const { drtDir } = await installer.findDrtDir(directory);
1618
+ const existingInstall = await ExistingInstall.detect(drtDir);
1619
+ return existingInstall.ides;
1620
+ }
1621
+
1622
+ /**
1623
+ * Display module versions with update availability
1624
+ * @param {Array} modules - Array of module info objects with version info
1625
+ * @param {Array} availableUpdates - Array of available updates
1626
+ */
1627
+ async displayModuleVersions(modules, availableUpdates = []) {
1628
+ // Group modules by source
1629
+ const builtIn = modules.filter((m) => m.source === 'built-in');
1630
+ const external = modules.filter((m) => m.source === 'external');
1631
+ const community = modules.filter((m) => m.source === 'community');
1632
+ const custom = modules.filter((m) => m.source === 'custom');
1633
+ const unknown = modules.filter((m) => m.source === 'unknown');
1634
+
1635
+ const lines = [];
1636
+ const formatGroup = (group, title) => {
1637
+ if (group.length === 0) return;
1638
+ lines.push(title);
1639
+ for (const mod of group) {
1640
+ const updateInfo = availableUpdates.find((u) => u.name === mod.name);
1641
+ const versionDisplay = mod.version || 'unknown';
1642
+ if (updateInfo) {
1643
+ lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2192 ${updateInfo.latestVersion} \u2191`);
1644
+ } else {
1645
+ lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2713`);
1646
+ }
1647
+ }
1648
+ };
1649
+
1650
+ formatGroup(builtIn, 'Built-in Modules');
1651
+ formatGroup(external, 'External Modules (Official)');
1652
+ formatGroup(community, 'Community Modules');
1653
+ formatGroup(custom, 'Custom Modules');
1654
+ formatGroup(unknown, 'Other Modules');
1655
+
1656
+ await prompts.note(lines.join('\n'), 'Module Versions');
1657
+ }
1658
+
1659
+ /**
1660
+ * Prompt user to select which modules to update
1661
+ * @param {Array} availableUpdates - Array of available updates
1662
+ * @returns {Array} Selected module names to update
1663
+ */
1664
+ async promptUpdateSelection(availableUpdates) {
1665
+ if (availableUpdates.length === 0) {
1666
+ return [];
1667
+ }
1668
+
1669
+ await prompts.log.info('Available Updates');
1670
+
1671
+ const choices = availableUpdates.map((update) => ({
1672
+ name: `${update.name} (v${update.installedVersion} \u2192 v${update.latestVersion})`,
1673
+ value: update.name,
1674
+ checked: true, // Default to selecting all updates
1675
+ }));
1676
+
1677
+ // Add "Update All" and "Cancel" options
1678
+ const action = await prompts.select({
1679
+ message: 'How would you like to proceed?',
1680
+ choices: [
1681
+ { name: 'Update all available modules', value: 'all' },
1682
+ { name: 'Select specific modules to update', value: 'select' },
1683
+ { name: 'Skip updates for now', value: 'skip' },
1684
+ ],
1685
+ default: 'all',
1686
+ });
1687
+
1688
+ if (action === 'all') {
1689
+ return availableUpdates.map((u) => u.name);
1690
+ }
1691
+
1692
+ if (action === 'skip') {
1693
+ return [];
1694
+ }
1695
+
1696
+ // Allow specific selection
1697
+ const selected = await prompts.multiselect({
1698
+ message: 'Select modules to update (use arrow keys, space to toggle):',
1699
+ choices: choices,
1700
+ required: true,
1701
+ });
1702
+
1703
+ return selected || [];
1704
+ }
1705
+
1706
+ /**
1707
+ * Display status of all installed modules
1708
+ * @param {Object} statusData - Status data with modules, installation info, and available updates
1709
+ */
1710
+ async displayStatus(statusData) {
1711
+ const { installation, modules, availableUpdates, drtDir } = statusData;
1712
+
1713
+ // Installation info
1714
+ const infoLines = [
1715
+ `Version: ${installation.version || 'unknown'}`,
1716
+ `Location: ${drtDir}`,
1717
+ `Installed: ${new Date(installation.installDate).toLocaleDateString()}`,
1718
+ `Last Updated: ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : 'unknown'}`,
1719
+ ];
1720
+
1721
+ await prompts.note(infoLines.join('\n'), 'DRT Status');
1722
+
1723
+ // Module versions
1724
+ await this.displayModuleVersions(modules, availableUpdates);
1725
+
1726
+ // Update summary
1727
+ if (availableUpdates.length > 0) {
1728
+ await prompts.log.warn(`${availableUpdates.length} update(s) available`);
1729
+ await prompts.log.message('Run \'npx drt-method install\' and select "Quick Update" to update');
1730
+ } else {
1731
+ await prompts.log.success('All modules are up to date');
1732
+ }
1733
+ }
1734
+
1735
+ /**
1736
+ * Display list of selected tools after IDE selection
1737
+ * @param {Array} selectedIdes - Array of selected IDE values
1738
+ * @param {Array} preferredIdes - Array of preferred IDE objects
1739
+ * @param {Array} allTools - Array of all tool objects
1740
+ */
1741
+ async displaySelectedTools(selectedIdes, preferredIdes, allTools) {
1742
+ if (selectedIdes.length === 0) return;
1743
+
1744
+ const preferredValues = new Set(preferredIdes.map((ide) => ide.value));
1745
+ const toolLines = selectedIdes.map((ideValue) => {
1746
+ const tool = allTools.find((t) => t.value === ideValue);
1747
+ const name = tool?.name || ideValue;
1748
+ const marker = preferredValues.has(ideValue) ? ' \u2B50' : '';
1749
+ return ` \u2022 ${name}${marker}`;
1750
+ });
1751
+ await prompts.log.message('Selected tools:\n' + toolLines.join('\n'));
1752
+ }
1753
+
1754
+ /**
1755
+ * Return the set of module codes the registry marks as built-in (core, bmm).
1756
+ * These ship with the installer binary and have no per-module channel.
1757
+ */
1758
+ async _bundledModuleCodes() {
1759
+ const externalManager = new ExternalModuleManager();
1760
+ try {
1761
+ const modules = await externalManager.listAvailable();
1762
+ return modules.filter((m) => m.builtIn).map((m) => m.code);
1763
+ } catch {
1764
+ // Registry unreachable — fall back to the known bundled codes.
1765
+ return ['core', 'drt'];
1766
+ }
1767
+ }
1768
+
1769
+ /**
1770
+ * Fast-path channel gate: confirm "all stable" or open the per-module picker.
1771
+ *
1772
+ * Skipped when:
1773
+ * - running non-interactively (--yes)
1774
+ * - the user already passed channel flags (--channel / --pin / --next), OR
1775
+ * the installer was launched from a prerelease (which seeds
1776
+ * channelOptions.global = 'next' upstream in promptInstall)
1777
+ * - no externals/community modules are selected
1778
+ *
1779
+ * Mutates channelOptions.pins and channelOptions.nextSet to reflect picker choices.
1780
+ */
1781
+ async _interactiveChannelGate({ options, channelOptions, selectedModules }) {
1782
+ if (options.yes) return;
1783
+ // If the user already declared their channel intent via flags, trust them
1784
+ // and skip the gate.
1785
+ const haveFlagIntent = channelOptions.global || channelOptions.nextSet.size > 0 || channelOptions.pins.size > 0;
1786
+ if (haveFlagIntent) return;
1787
+
1788
+ // Figure out which selected modules actually get a channel (externals only).
1789
+ // Bundled core/bmm and custom modules skip the picker.
1790
+ const externalManager = new ExternalModuleManager();
1791
+ const externals = await externalManager.listAvailable();
1792
+ const externalByCode = new Map(externals.map((m) => [m.code, m]));
1793
+
1794
+ const channelSelectable = selectedModules.filter((code) => {
1795
+ const info = externalByCode.get(code);
1796
+ return info && !info.builtIn;
1797
+ });
1798
+ if (channelSelectable.length === 0) return;
1799
+
1800
+ const fastPath = await prompts.confirm({
1801
+ message: `Ready to install (all stable)? Pick "n" to customize channels or pin versions.`,
1802
+ default: true,
1803
+ });
1804
+ if (fastPath) return; // stable for all, registry default applies
1805
+
1806
+ // Customize path: per-module picker.
1807
+ const { fetchStableTags, parseGitHubRepo } = require('./modules/channel-resolver');
1808
+
1809
+ for (const code of channelSelectable) {
1810
+ const info = externalByCode.get(code);
1811
+ const repoUrl = info.url;
1812
+
1813
+ // Try to pre-resolve the top stable tag so we can surface it in the picker.
1814
+ let stableLabel = 'stable (released version)';
1815
+ try {
1816
+ const parsed = repoUrl ? parseGitHubRepo(repoUrl) : null;
1817
+ if (parsed) {
1818
+ const tags = await fetchStableTags(parsed.owner, parsed.repo);
1819
+ if (tags.length > 0) {
1820
+ stableLabel = `stable ${tags[0].tag} (released version)`;
1821
+ }
1822
+ }
1823
+ } catch {
1824
+ // fall through with the generic label
1825
+ }
1826
+
1827
+ const choice = await prompts.select({
1828
+ message: `${code}: choose a channel`,
1829
+ choices: [
1830
+ { name: stableLabel, value: 'stable' },
1831
+ { name: 'next (main HEAD \u2014 current development)', value: 'next' },
1832
+ { name: 'pin (specific version)', value: 'pin' },
1833
+ ],
1834
+ default: 'stable',
1835
+ });
1836
+
1837
+ if (choice === 'next') {
1838
+ channelOptions.nextSet.add(code);
1839
+ } else if (choice === 'pin') {
1840
+ const pinValue = await prompts.text({
1841
+ message: `Enter a version tag for '${code}' (e.g. v1.6.0):`,
1842
+ validate: (value) => {
1843
+ if (!value || !/^[\w.\-+/]+$/.test(String(value).trim())) {
1844
+ return 'Must be a non-empty tag name (letters, digits, dots, hyphens).';
1845
+ }
1846
+ },
1847
+ });
1848
+ channelOptions.pins.set(code, String(pinValue).trim());
1849
+ }
1850
+ // 'stable' is the default; nothing to record.
1851
+ }
1852
+ }
1853
+
1854
+ /**
1855
+ * Resolve channel decisions for an update over an existing install.
1856
+ *
1857
+ * For each selected external/community module:
1858
+ * - Read the recorded channel from the existing manifest.
1859
+ * - On `stable`: query tags; if a newer stable exists, classify the diff
1860
+ * and prompt. Patch/minor default Y; major defaults N. `--yes` accepts
1861
+ * defaults (patches/minors) but NOT majors — a major under --yes stays
1862
+ * frozen unless the user also passes `--pin CODE=NEW_TAG`.
1863
+ * - On `next`: no prompt (pull HEAD).
1864
+ * - On `pinned`: no prompt (stays pinned).
1865
+ * - No channel recorded and `version: null`: one-time migration prompt
1866
+ * ("Switch to stable / Keep on next").
1867
+ *
1868
+ * Decisions that freeze the current version are applied by adding a pin to
1869
+ * `channelOptions.pins` so downstream clone logic honors them.
1870
+ */
1871
+ async _resolveUpdateChannels({ drtDir, selectedModules, channelOptions, yes }) {
1872
+ const { Manifest } = require('./core/manifest');
1873
+ const manifestObj = new Manifest();
1874
+ const manifest = await manifestObj.read(drtDir);
1875
+ const existingByName = new Map();
1876
+ for (const m of manifest?.modulesDetailed || []) {
1877
+ if (m?.name) existingByName.set(m.name, m);
1878
+ }
1879
+ if (existingByName.size === 0) return;
1880
+
1881
+ const externalManager = new ExternalModuleManager();
1882
+ const externals = await externalManager.listAvailable();
1883
+ const externalByCode = new Map(externals.map((m) => [m.code, m]));
1884
+
1885
+ const { fetchStableTags, classifyUpgrade, releaseNotesUrl } = require('./modules/channel-resolver');
1886
+ const { parseGitHubRepo } = require('./modules/channel-resolver');
1887
+
1888
+ // Interactive-only: offer a one-time gate to review / switch channels for
1889
+ // selected modules that are already installed. Default N so normal Modify
1890
+ // flows (add/remove modules) aren't interrupted.
1891
+ let reviewChannels = false;
1892
+ if (!yes) {
1893
+ const existingWithChannel = selectedModules.filter((code) => {
1894
+ const prev = existingByName.get(code);
1895
+ if (!prev) return false;
1896
+ const info = externalByCode.get(code);
1897
+ return info && !info.builtIn;
1898
+ });
1899
+ if (existingWithChannel.length > 0) {
1900
+ reviewChannels = await prompts.confirm({
1901
+ message: 'Review channel assignments (stable / next / pin) for your existing modules?',
1902
+ default: false,
1903
+ });
1904
+ }
1905
+ }
1906
+
1907
+ for (const code of selectedModules) {
1908
+ const prev = existingByName.get(code);
1909
+ if (!prev) continue;
1910
+
1911
+ const info = externalByCode.get(code);
1912
+ if (!info) continue;
1913
+ // Bundled modules (core/bmm) ship with the installer binary itself —
1914
+ // their version is stapled to the CLI version, not a git tag. Skip
1915
+ // tag-API lookups for them; the "upgrade" mechanism is `npx drt@X install`.
1916
+ if (info.builtIn) continue;
1917
+
1918
+ const repoUrl = info.url;
1919
+ const parsed = repoUrl ? parseGitHubRepo(repoUrl) : null;
1920
+
1921
+ // Legacy migration: manifest carries no channel and a null/empty
1922
+ // version. Offer the one-time pick between stable and next.
1923
+ const recordedChannel = prev.channel || null;
1924
+ const needsMigration = !recordedChannel && (prev.version == null || prev.version === '');
1925
+ if (needsMigration) {
1926
+ if (yes) {
1927
+ // Conservative headless default: stable.
1928
+ continue;
1929
+ }
1930
+ const chosen = await prompts.select({
1931
+ message: `${code}: your existing install tracks the main branch. Switch to stable releases (recommended for production), or keep on main?`,
1932
+ choices: [
1933
+ { name: 'Switch to stable', value: 'stable' },
1934
+ { name: 'Keep on main (next)', value: 'next' },
1935
+ ],
1936
+ default: 'stable',
1937
+ });
1938
+ if (chosen === 'next') channelOptions.nextSet.add(code);
1939
+ continue;
1940
+ }
1941
+
1942
+ // Optional channel-switch offer. Fires only when the user opted in via
1943
+ // the gate above. 'keep' falls through to the existing per-channel
1944
+ // logic (which runs upgrade classification for stable). Any switch
1945
+ // records the new intent into channelOptions and skips upgrade prompts.
1946
+ if (reviewChannels && recordedChannel) {
1947
+ const switchChoices = [
1948
+ {
1949
+ name: `Keep on '${recordedChannel}'${prev.version ? ` @ ${prev.version}` : ''}`,
1950
+ value: 'keep',
1951
+ },
1952
+ ];
1953
+ if (recordedChannel !== 'stable') {
1954
+ switchChoices.push({ name: 'Switch to stable (released version)', value: 'stable' });
1955
+ }
1956
+ if (recordedChannel !== 'next') {
1957
+ switchChoices.push({ name: 'Switch to next (main HEAD)', value: 'next' });
1958
+ }
1959
+ switchChoices.push({ name: 'Pin to a specific version tag', value: 'pin' });
1960
+
1961
+ const choice = await prompts.select({
1962
+ message: `${code} channel:`,
1963
+ choices: switchChoices,
1964
+ default: 'keep',
1965
+ });
1966
+
1967
+ if (choice === 'next') {
1968
+ channelOptions.nextSet.add(code);
1969
+ continue;
1970
+ }
1971
+ if (choice === 'pin') {
1972
+ const pinValue = await prompts.text({
1973
+ message: `Enter a version tag for '${code}' (e.g. v1.6.0):`,
1974
+ validate: (value) => {
1975
+ if (!value || !/^[\w.\-+/]+$/.test(String(value).trim())) {
1976
+ return 'Must be a non-empty tag name (letters, digits, dots, hyphens).';
1977
+ }
1978
+ },
1979
+ });
1980
+ channelOptions.pins.set(code, String(pinValue).trim());
1981
+ continue;
1982
+ }
1983
+ if (choice === 'stable') {
1984
+ // Switch to stable: install at the top stable tag without an
1985
+ // upgrade-classification prompt (the user explicitly opted in).
1986
+ // Also warm the tag cache here so the actual clone step doesn't
1987
+ // need a second GitHub API call (can hit rate limits).
1988
+ if (parsed) {
1989
+ try {
1990
+ await fetchStableTags(parsed.owner, parsed.repo);
1991
+ } catch {
1992
+ // best effort; clone step will surface any failure
1993
+ }
1994
+ }
1995
+ continue;
1996
+ }
1997
+ // 'keep' → fall through with recordedChannel below.
1998
+ }
1999
+
2000
+ if (recordedChannel === 'pinned' || recordedChannel === 'next') {
2001
+ // Respect any explicit channel intent the user already expressed via
2002
+ // CLI flags (--channel / --all-* / --next=CODE / --pin CODE=TAG) or
2003
+ // via the interactive review gate above. Only auto-re-assert the
2004
+ // recorded channel when the user hasn't opted into anything else —
2005
+ // otherwise --all-stable (or a review "switch to stable") would be
2006
+ // silently clobbered by the prior channel.
2007
+ const alreadyDecided = channelOptions.global || channelOptions.nextSet.has(code) || channelOptions.pins.has(code);
2008
+ if (!alreadyDecided) {
2009
+ if (recordedChannel === 'pinned' && prev.version) {
2010
+ channelOptions.pins.set(code, prev.version);
2011
+ } else if (recordedChannel === 'next') {
2012
+ channelOptions.nextSet.add(code);
2013
+ }
2014
+ }
2015
+ continue;
2016
+ }
2017
+
2018
+ // Stable channel: check for a newer released tag.
2019
+ if (!parsed) continue;
2020
+ // Respect explicit CLI intent (--pin / --next=CODE / --all-*) and any
2021
+ // choice the user already made in the earlier review gate. Without this
2022
+ // guard the upgrade classifier below would unconditionally call
2023
+ // `channelOptions.pins.set(code, prev.version)` on decline/major-refuse/
2024
+ // fetch-error, silently clobbering the user's override.
2025
+ const alreadyDecided = channelOptions.global || channelOptions.nextSet.has(code) || channelOptions.pins.has(code);
2026
+ if (alreadyDecided) continue;
2027
+ let tags;
2028
+ try {
2029
+ tags = await fetchStableTags(parsed.owner, parsed.repo);
2030
+ } catch (error) {
2031
+ await prompts.log.warn(`Could not check for updates on ${code} (${error.message}). Leaving at ${prev.version}.`);
2032
+ if (prev.version) channelOptions.pins.set(code, prev.version);
2033
+ continue;
2034
+ }
2035
+ if (!tags || tags.length === 0) continue;
2036
+ const topTag = tags[0].tag; // e.g. "v1.7.0"
2037
+ const currentTag = prev.version || '';
2038
+ const diffClass = classifyUpgrade(currentTag, topTag);
2039
+
2040
+ if (diffClass === 'none') continue; // already at or above top tag
2041
+
2042
+ const notes = releaseNotesUrl(repoUrl, topTag);
2043
+ let accept;
2044
+ if (diffClass === 'major') {
2045
+ if (yes) {
2046
+ // Major under --yes is refused by design.
2047
+ await prompts.log.warn(
2048
+ `${code} ${currentTag} → ${topTag} is a new major release; staying on ${currentTag}. ` +
2049
+ `To accept, rerun with --pin ${code}=${topTag}.`,
2050
+ );
2051
+ channelOptions.pins.set(code, currentTag);
2052
+ continue;
2053
+ }
2054
+ accept = await prompts.confirm({
2055
+ message:
2056
+ `${code} ${topTag} available — new major release (may change behavior).` +
2057
+ (notes ? ` Release notes: ${notes}.` : '') +
2058
+ ' Upgrade?',
2059
+ default: false,
2060
+ });
2061
+ } else if (diffClass === 'minor') {
2062
+ if (yes) {
2063
+ accept = true;
2064
+ } else {
2065
+ accept = await prompts.confirm({
2066
+ message: `${code} ${topTag} available (new features).` + (notes ? ` Release notes: ${notes}.` : '') + ' Upgrade?',
2067
+ default: true,
2068
+ });
2069
+ }
2070
+ } else {
2071
+ // patch
2072
+ if (yes) {
2073
+ accept = true;
2074
+ } else {
2075
+ accept = await prompts.confirm({
2076
+ message: `${code} ${topTag} available. Upgrade?`,
2077
+ default: true,
2078
+ });
2079
+ }
2080
+ }
2081
+
2082
+ if (!accept && currentTag) {
2083
+ // Freeze the current version by pinning it for this run.
2084
+ channelOptions.pins.set(code, currentTag);
2085
+ }
2086
+ }
2087
+ }
2088
+ }
2089
+
2090
+ module.exports = { UI };