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