mdan-cli 2.5.0 → 2.5.2
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/.augment/code_review_guidelines.yaml +271 -0
- package/.claude/skills/bmad-os-audit-file-refs/SKILL.md +6 -0
- package/.claude/skills/bmad-os-audit-file-refs/prompts/instructions.md +59 -0
- package/.claude/skills/bmad-os-changelog-social/SKILL.md +177 -0
- package/.claude/skills/bmad-os-changelog-social/examples/discord-example.md +53 -0
- package/.claude/skills/bmad-os-changelog-social/examples/linkedin-example.md +49 -0
- package/.claude/skills/bmad-os-changelog-social/examples/twitter-example.md +55 -0
- package/.claude/skills/bmad-os-diataxis-style-fix/SKILL.md +6 -0
- package/.claude/skills/bmad-os-diataxis-style-fix/prompts/instructions.md +229 -0
- package/.claude/skills/bmad-os-draft-changelog/SKILL.md +6 -0
- package/.claude/skills/bmad-os-draft-changelog/prompts/instructions.md +82 -0
- package/.claude/skills/bmad-os-gh-triage/SKILL.md +6 -0
- package/.claude/skills/bmad-os-gh-triage/prompts/agent-prompt.md +60 -0
- package/.claude/skills/bmad-os-gh-triage/prompts/instructions.md +74 -0
- package/.claude/skills/bmad-os-release-module/SKILL.md +6 -0
- package/.claude/skills/bmad-os-release-module/prompts/instructions.md +53 -0
- package/.claude/skills/bmad-os-review-pr/SKILL.md +6 -0
- package/.claude/skills/bmad-os-review-pr/prompts/instructions.md +231 -0
- package/.claude/skills/bmad-os-root-cause-analysis/SKILL.md +12 -0
- package/.claude/skills/bmad-os-root-cause-analysis/prompts/instructions.md +74 -0
- package/.coderabbit.yaml +85 -0
- package/.github/CODE_OF_CONDUCT.md +128 -0
- package/.github/FUNDING.yaml +15 -0
- package/.github/ISSUE_TEMPLATE/bug-report.yaml +124 -0
- package/.github/ISSUE_TEMPLATE/config.yaml +8 -0
- package/.github/ISSUE_TEMPLATE/documentation.yaml +55 -0
- package/.github/ISSUE_TEMPLATE/feature-request.md +22 -0
- package/.github/ISSUE_TEMPLATE/issue.md +32 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/.github/scripts/discord-helpers.sh +34 -0
- package/.github/workflows/coderabbit-review.yaml +22 -0
- package/.github/workflows/discord.yaml +90 -0
- package/.github/workflows/docs.yaml +64 -0
- package/.github/workflows/quality.yaml +116 -0
- package/.husky/pre-commit +20 -0
- package/.markdownlint-cli2.yaml +41 -0
- package/.nvmrc +1 -0
- package/.prettierignore +12 -0
- package/.vscode/settings.json +96 -0
- package/AGENTS.md +227 -165
- package/AGENTS_LIST.md +946 -0
- package/ARCHITECTURE.md +590 -0
- package/CHANGELOG.md +1770 -0
- package/CNAME +1 -0
- package/CONTRIBUTING.md +512 -0
- package/CONTRIBUTORS.md +32 -0
- package/INSTALL.md +246 -0
- package/LICENSE +30 -0
- package/README.md +133 -194
- package/RELEASE_NOTES.md +246 -0
- package/SECURITY.md +85 -0
- package/TRADEMARK.md +55 -0
- package/USAGE.md +368 -0
- package/Wordmark.png +0 -0
- package/app/__init__.py +5 -0
- package/app/cis/agents/__init__.py +31 -0
- package/app/cis/agents/brainstorming-coach/__init__.py +3 -0
- package/app/cis/agents/brainstorming-coach/agent.py +162 -0
- package/app/cis/agents/brainstorming-coach/prompt.yaml +53 -0
- package/app/cis/agents/creative-problem-solver/__init__.py +3 -0
- package/app/cis/agents/creative-problem-solver/agent.py +233 -0
- package/app/cis/agents/creative-problem-solver/prompt.yaml +74 -0
- package/app/cis/agents/design-thinking-coach/__init__.py +3 -0
- package/app/cis/agents/design-thinking-coach/agent.py +241 -0
- package/app/cis/agents/design-thinking-coach/prompt.yaml +77 -0
- package/app/cis/agents/innovation-strategist/__init__.py +3 -0
- package/app/cis/agents/innovation-strategist/agent.py +271 -0
- package/app/cis/agents/innovation-strategist/prompt.yaml +70 -0
- package/app/cis/agents/presentation-master/__init__.py +3 -0
- package/app/cis/agents/presentation-master/agent.py +420 -0
- package/app/cis/agents/presentation-master/prompt.yaml +62 -0
- package/app/cis/agents/storyteller/__init__.py +3 -0
- package/app/cis/agents/storyteller/agent.py +303 -0
- package/app/cis/agents/storyteller/prompt.yaml +99 -0
- package/app/core/__init__.py +5 -0
- package/app/core/agents/__init__.py +5 -0
- package/app/core/agents/mdan-master/__init__.py +7 -0
- package/app/core/agents/mdan-master/agent.py +302 -0
- package/app/core/agents/mdan-master/prompt.yaml +105 -0
- package/app/mmb/agents/__init__.py +24 -0
- package/app/mmb/agents/agent-builder/__init__.py +5 -0
- package/app/mmb/agents/agent-builder/agent.py +261 -0
- package/app/mmb/agents/agent-builder/prompt.yaml +48 -0
- package/app/mmb/agents/module-builder/__init__.py +5 -0
- package/app/mmb/agents/module-builder/agent.py +299 -0
- package/app/mmb/agents/module-builder/prompt.yaml +50 -0
- package/app/mmb/agents/workflow-builder/__init__.py +5 -0
- package/app/mmb/agents/workflow-builder/agent.py +318 -0
- package/app/mmb/agents/workflow-builder/prompt.yaml +52 -0
- package/app/mmm/agents/__init__.py +48 -0
- package/app/mmm/agents/analyst/__init__.py +7 -0
- package/app/mmm/agents/analyst/agent.py +384 -0
- package/app/mmm/agents/analyst/prompt.yaml +62 -0
- package/app/mmm/agents/architect/__init__.py +7 -0
- package/app/mmm/agents/architect/agent.py +300 -0
- package/app/mmm/agents/architect/prompt.yaml +66 -0
- package/app/mmm/agents/dev/__init__.py +7 -0
- package/app/mmm/agents/dev/agent.py +285 -0
- package/app/mmm/agents/dev/prompt.yaml +62 -0
- package/app/mmm/agents/pm/__init__.py +7 -0
- package/app/mmm/agents/pm/agent.py +417 -0
- package/app/mmm/agents/pm/prompt.yaml +64 -0
- package/app/mmm/agents/qa/__init__.py +7 -0
- package/app/mmm/agents/qa/agent.py +267 -0
- package/app/mmm/agents/qa/prompt.yaml +67 -0
- package/app/mmm/agents/quick-flow-solo-dev/__init__.py +7 -0
- package/app/mmm/agents/quick-flow-solo-dev/agent.py +319 -0
- package/app/mmm/agents/quick-flow-solo-dev/prompt.yaml +60 -0
- package/app/mmm/agents/sm/__init__.py +7 -0
- package/app/mmm/agents/sm/agent.py +357 -0
- package/app/mmm/agents/sm/prompt.yaml +61 -0
- package/app/mmm/agents/tech-writer/__init__.py +7 -0
- package/app/mmm/agents/tech-writer/agent.py +420 -0
- package/app/mmm/agents/tech-writer/prompt.yaml +70 -0
- package/app/mmm/agents/ux-designer/__init__.py +14 -0
- package/app/mmm/agents/ux-designer/agent.py +412 -0
- package/app/mmm/agents/ux-designer/prompt.yaml +37 -0
- package/app/packs/__init__.py +32 -0
- package/app/packs/db-optimization/__init__.py +13 -0
- package/app/packs/db-optimization/agents/__init__.py +11 -0
- package/app/packs/db-optimization/agents/db-performance-analyst/__init__.py +5 -0
- package/app/packs/db-optimization/agents/db-performance-analyst/agent.py +559 -0
- package/app/packs/db-optimization/agents/db-performance-analyst/prompt.yaml +63 -0
- package/app/packs/db-optimization/agents/indexing-specialist/__init__.py +5 -0
- package/app/packs/db-optimization/agents/indexing-specialist/agent.py +713 -0
- package/app/packs/db-optimization/agents/indexing-specialist/prompt.yaml +92 -0
- package/app/packs/db-optimization/agents/query-optimizer/__init__.py +5 -0
- package/app/packs/db-optimization/agents/query-optimizer/agent.py +566 -0
- package/app/packs/db-optimization/agents/query-optimizer/prompt.yaml +74 -0
- package/app/packs/devops-azure/__init__.py +13 -0
- package/app/packs/devops-azure/agents/__init__.py +11 -0
- package/app/packs/devops-azure/agents/azure-specialist/__init__.py +5 -0
- package/app/packs/devops-azure/agents/azure-specialist/agent.py +584 -0
- package/app/packs/devops-azure/agents/azure-specialist/prompt.yaml +301 -0
- package/app/packs/devops-azure/agents/cicd-architect/__init__.py +5 -0
- package/app/packs/devops-azure/agents/cicd-architect/agent.py +665 -0
- package/app/packs/devops-azure/agents/cicd-architect/prompt.yaml +409 -0
- package/app/packs/devops-azure/agents/devops-engineer/__init__.py +5 -0
- package/app/packs/devops-azure/agents/devops-engineer/agent.py +545 -0
- package/app/packs/devops-azure/agents/devops-engineer/prompt.yaml +263 -0
- package/app/packs/fintech/__init__.py +13 -0
- package/app/packs/fintech/agents/__init__.py +11 -0
- package/app/packs/fintech/agents/compliance-officer/__init__.py +5 -0
- package/app/packs/fintech/agents/compliance-officer/agent.py +449 -0
- package/app/packs/fintech/agents/compliance-officer/prompt.yaml +135 -0
- package/app/packs/fintech/agents/financial-analyst/__init__.py +5 -0
- package/app/packs/fintech/agents/financial-analyst/agent.py +392 -0
- package/app/packs/fintech/agents/financial-analyst/prompt.yaml +143 -0
- package/app/packs/fintech/agents/risk-manager/__init__.py +5 -0
- package/app/packs/fintech/agents/risk-manager/agent.py +664 -0
- package/app/packs/fintech/agents/risk-manager/prompt.yaml +240 -0
- package/app/tea/agents/tea/__init__.py +9 -0
- package/app/tea/agents/tea/agent.py +689 -0
- package/app/tea/agents/tea/prompt.yaml +100 -0
- package/banner-bmad-method.png +0 -0
- package/docs/404.md +9 -0
- package/docs/_STYLE_GUIDE.md +370 -0
- package/docs/explanation/advanced-elicitation.md +49 -0
- package/docs/explanation/adversarial-review.md +59 -0
- package/docs/explanation/brainstorming.md +33 -0
- package/docs/explanation/established-projects-faq.md +50 -0
- package/docs/explanation/party-mode.md +59 -0
- package/docs/explanation/preventing-agent-conflicts.md +112 -0
- package/docs/explanation/project-context.md +157 -0
- package/docs/explanation/quick-flow.md +73 -0
- package/docs/explanation/why-solutioning-matters.md +77 -0
- package/docs/how-to/customize-bmad.md +172 -0
- package/docs/how-to/established-projects.md +117 -0
- package/docs/how-to/get-answers-about-bmad.md +134 -0
- package/docs/how-to/install-bmad.md +97 -0
- package/docs/how-to/non-interactive-installation.md +171 -0
- package/docs/how-to/project-context.md +136 -0
- package/docs/how-to/quick-fixes.md +123 -0
- package/docs/how-to/shard-large-documents.md +78 -0
- package/docs/how-to/upgrade-to-v6.md +97 -0
- package/docs/index.md +59 -0
- package/docs/reference/agents.md +28 -0
- package/docs/reference/commands.md +151 -0
- package/docs/reference/modules.md +76 -0
- package/docs/reference/testing.md +106 -0
- package/docs/reference/workflow-map.md +89 -0
- package/docs/roadmap.mdx +136 -0
- package/docs/tutorials/getting-started.md +286 -0
- package/eslint.config.mjs +141 -0
- package/package.json +106 -37
- package/prettier.config.mjs +32 -0
- package/prompts/cis/brainstorming-coach.yaml +53 -0
- package/prompts/cis/creative-problem-solver.yaml +74 -0
- package/prompts/cis/design-thinking-coach.yaml +77 -0
- package/prompts/cis/innovation-strategist.yaml +70 -0
- package/prompts/cis/presentation-master.yaml +62 -0
- package/prompts/cis/storyteller.yaml +99 -0
- package/prompts/core/mdan-master.yaml +105 -0
- package/prompts/mmb/agent-builder.yaml +48 -0
- package/prompts/mmb/module-builder.yaml +50 -0
- package/prompts/mmb/workflow-builder.yaml +52 -0
- package/prompts/mmm/analyst.yaml +62 -0
- package/prompts/mmm/architect.yaml +66 -0
- package/prompts/mmm/dev.yaml +62 -0
- package/prompts/mmm/pm.yaml +64 -0
- package/prompts/mmm/qa.yaml +67 -0
- package/prompts/mmm/quick-flow-solo-dev.yaml +60 -0
- package/prompts/mmm/sm.yaml +61 -0
- package/prompts/mmm/tech-writer.yaml +70 -0
- package/prompts/mmm/ux-designer.yaml +33 -0
- package/prompts/packs/db-optimization/db-performance-analyst.yaml +63 -0
- package/prompts/packs/db-optimization/indexing-specialist.yaml +92 -0
- package/prompts/packs/db-optimization/query-optimizer.yaml +74 -0
- package/prompts/packs/devops-azure/azure-specialist.yaml +301 -0
- package/prompts/packs/devops-azure/cicd-architect.yaml +409 -0
- package/prompts/packs/devops-azure/devops-engineer.yaml +263 -0
- package/prompts/packs/fintech/compliance-officer.yaml +135 -0
- package/prompts/packs/fintech/financial-analyst.yaml +143 -0
- package/prompts/packs/fintech/risk-manager.yaml +240 -0
- package/prompts/tea/tea.yaml +100 -0
- package/prompts.json +237 -0
- package/src/bmm/agents/analyst.agent.yaml +43 -0
- package/src/bmm/agents/architect.agent.yaml +29 -0
- package/src/bmm/agents/dev.agent.yaml +38 -0
- package/src/bmm/agents/pm.agent.yaml +44 -0
- package/src/bmm/agents/qa.agent.yaml +58 -0
- package/src/bmm/agents/quick-flow-solo-dev.agent.yaml +32 -0
- package/src/bmm/agents/sm.agent.yaml +37 -0
- package/src/bmm/agents/tech-writer/tech-writer-sidecar/documentation-standards.md +224 -0
- package/src/bmm/agents/tech-writer/tech-writer.agent.yaml +46 -0
- package/src/bmm/agents/ux-designer.agent.yaml +27 -0
- package/src/bmm/data/project-context-template.md +26 -0
- package/src/bmm/module-help.csv +31 -0
- package/src/bmm/module.yaml +50 -0
- package/src/bmm/teams/default-party.csv +20 -0
- package/src/bmm/teams/team-fullstack.yaml +12 -0
- package/src/bmm/workflows/1-analysis/create-product-brief/product-brief.template.md +10 -0
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-01-init.md +177 -0
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-01b-continue.md +161 -0
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-02-vision.md +199 -0
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-03-users.md +202 -0
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-04-metrics.md +205 -0
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-05-scope.md +219 -0
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-06-complete.md +162 -0
- package/src/bmm/workflows/1-analysis/create-product-brief/workflow.md +57 -0
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-01-init.md +137 -0
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-02-domain-analysis.md +229 -0
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-03-competitive-landscape.md +238 -0
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-04-regulatory-focus.md +206 -0
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-05-technical-trends.md +234 -0
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-06-research-synthesis.md +444 -0
- package/src/bmm/workflows/1-analysis/research/market-steps/step-01-init.md +182 -0
- package/src/bmm/workflows/1-analysis/research/market-steps/step-02-customer-behavior.md +237 -0
- package/src/bmm/workflows/1-analysis/research/market-steps/step-03-customer-pain-points.md +249 -0
- package/src/bmm/workflows/1-analysis/research/market-steps/step-04-customer-decisions.md +259 -0
- package/src/bmm/workflows/1-analysis/research/market-steps/step-05-competitive-analysis.md +177 -0
- package/src/bmm/workflows/1-analysis/research/market-steps/step-06-research-completion.md +476 -0
- package/src/bmm/workflows/1-analysis/research/research.template.md +29 -0
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-01-init.md +137 -0
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-02-technical-overview.md +239 -0
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-03-integration-patterns.md +248 -0
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-04-architectural-patterns.md +202 -0
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-05-implementation-research.md +233 -0
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-06-research-synthesis.md +487 -0
- package/src/bmm/workflows/1-analysis/research/workflow-domain-research.md +54 -0
- package/src/bmm/workflows/1-analysis/research/workflow-market-research.md +54 -0
- package/src/bmm/workflows/1-analysis/research/workflow-technical-research.md +54 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/data/domain-complexity.csv +15 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md +197 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/data/project-types.csv +11 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-01-init.md +191 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-01b-continue.md +152 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02-discovery.md +224 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02b-vision.md +154 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02c-executive-summary.md +170 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-03-success.md +226 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-04-journeys.md +213 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-05-domain.md +207 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-06-innovation.md +226 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-07-project-type.md +237 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-08-scoping.md +228 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-09-functional.md +231 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-10-nonfunctional.md +242 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-11-polish.md +217 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-12-complete.md +124 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-01-discovery.md +247 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-01b-legacy-conversion.md +208 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-02-review.md +249 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-03-edit.md +253 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-04-complete.md +168 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-01-discovery.md +226 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-02-format-detection.md +191 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-02b-parity-check.md +209 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-03-density-validation.md +174 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-04-brief-coverage-validation.md +214 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-05-measurability-validation.md +228 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-06-traceability-validation.md +217 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-07-implementation-leakage-validation.md +205 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-08-domain-compliance-validation.md +243 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-09-project-type-validation.md +263 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-10-smart-validation.md +209 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-11-holistic-quality-validation.md +264 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-12-completeness-validation.md +242 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-13-report-complete.md +231 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/templates/prd-template.md +10 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md +63 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md +65 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md +63 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-01-init.md +135 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-01b-continue.md +127 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-02-discovery.md +190 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-03-core-experience.md +216 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-04-emotional-response.md +219 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-05-inspiration.md +234 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-06-design-system.md +252 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-07-defining-experience.md +254 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-08-visual-foundation.md +224 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-09-design-directions.md +224 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-10-user-journeys.md +241 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-11-component-strategy.md +248 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-12-ux-patterns.md +237 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-13-responsive-accessibility.md +264 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-14-complete.md +171 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/ux-design-template.md +13 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md +42 -0
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-01-document-discovery.md +184 -0
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-02-prd-analysis.md +172 -0
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-03-epic-coverage-validation.md +173 -0
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-04-ux-alignment.md +133 -0
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-05-epic-quality-review.md +245 -0
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-06-final-assessment.md +129 -0
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/templates/readiness-report-template.md +4 -0
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md +54 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/architecture-decision-template.md +12 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/data/domain-complexity.csv +13 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/data/project-types.csv +7 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-01-init.md +153 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-01b-continue.md +173 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-02-context.md +224 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-03-starter.md +329 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-04-decisions.md +318 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-05-patterns.md +359 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-06-structure.md +379 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-07-validation.md +359 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-08-complete.md +76 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/workflow.md +49 -0
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-01-validate-prerequisites.md +259 -0
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-02-design-epics.md +233 -0
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-03-create-stories.md +272 -0
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-04-final-validation.md +149 -0
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/templates/epics-template.md +57 -0
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md +58 -0
- package/src/bmm/workflows/4-implementation/code-review/checklist.md +23 -0
- package/src/bmm/workflows/4-implementation/code-review/instructions.xml +227 -0
- package/src/bmm/workflows/4-implementation/code-review/workflow.yaml +43 -0
- package/src/bmm/workflows/4-implementation/correct-course/checklist.md +288 -0
- package/src/bmm/workflows/4-implementation/correct-course/instructions.md +207 -0
- package/src/bmm/workflows/4-implementation/correct-course/workflow.yaml +53 -0
- package/src/bmm/workflows/4-implementation/create-story/checklist.md +358 -0
- package/src/bmm/workflows/4-implementation/create-story/instructions.xml +346 -0
- package/src/bmm/workflows/4-implementation/create-story/template.md +49 -0
- package/src/bmm/workflows/4-implementation/create-story/workflow.yaml +52 -0
- package/src/bmm/workflows/4-implementation/dev-story/checklist.md +80 -0
- package/src/bmm/workflows/4-implementation/dev-story/instructions.xml +410 -0
- package/src/bmm/workflows/4-implementation/dev-story/workflow.yaml +20 -0
- package/src/bmm/workflows/4-implementation/retrospective/instructions.md +1444 -0
- package/src/bmm/workflows/4-implementation/retrospective/workflow.yaml +52 -0
- package/src/bmm/workflows/4-implementation/sprint-planning/checklist.md +33 -0
- package/src/bmm/workflows/4-implementation/sprint-planning/instructions.md +226 -0
- package/src/bmm/workflows/4-implementation/sprint-planning/sprint-status-template.yaml +55 -0
- package/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml +47 -0
- package/src/bmm/workflows/4-implementation/sprint-status/instructions.md +230 -0
- package/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml +25 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-01-mode-detection.md +174 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-02-context-gathering.md +118 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-03-execute.md +111 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-04-self-check.md +111 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-05-adversarial-review.md +104 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-06-resolve-findings.md +146 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md +50 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-01-understand.md +189 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-02-investigate.md +143 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-03-generate.md +126 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md +200 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/tech-spec-template.md +74 -0
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md +79 -0
- package/src/bmm/workflows/document-project/checklist.md +245 -0
- package/src/bmm/workflows/document-project/documentation-requirements.csv +12 -0
- package/src/bmm/workflows/document-project/instructions.md +130 -0
- package/src/bmm/workflows/document-project/templates/deep-dive-template.md +345 -0
- package/src/bmm/workflows/document-project/templates/index-template.md +169 -0
- package/src/bmm/workflows/document-project/templates/project-overview-template.md +103 -0
- package/src/bmm/workflows/document-project/templates/project-scan-report-schema.json +160 -0
- package/src/bmm/workflows/document-project/templates/source-tree-template.md +135 -0
- package/src/bmm/workflows/document-project/workflow.yaml +22 -0
- package/src/bmm/workflows/document-project/workflows/deep-dive-instructions.md +298 -0
- package/src/bmm/workflows/document-project/workflows/deep-dive.yaml +31 -0
- package/src/bmm/workflows/document-project/workflows/full-scan-instructions.md +1106 -0
- package/src/bmm/workflows/document-project/workflows/full-scan.yaml +31 -0
- package/src/bmm/workflows/generate-project-context/project-context-template.md +21 -0
- package/src/bmm/workflows/generate-project-context/steps/step-01-discover.md +184 -0
- package/src/bmm/workflows/generate-project-context/steps/step-02-generate.md +318 -0
- package/src/bmm/workflows/generate-project-context/steps/step-03-complete.md +278 -0
- package/src/bmm/workflows/generate-project-context/workflow.md +49 -0
- package/src/bmm/workflows/qa-generate-e2e-tests/checklist.md +33 -0
- package/src/bmm/workflows/qa-generate-e2e-tests/instructions.md +110 -0
- package/src/bmm/workflows/qa-generate-e2e-tests/workflow.yaml +42 -0
- package/src/core/agents/bmad-master.agent.yaml +30 -0
- package/src/core/module-help.csv +9 -0
- package/src/core/module.yaml +25 -0
- package/src/core/tasks/editorial-review-prose.xml +102 -0
- package/src/core/tasks/editorial-review-structure.xml +208 -0
- package/src/core/tasks/help.md +86 -0
- package/src/core/tasks/index-docs.xml +65 -0
- package/src/core/tasks/review-adversarial-general.xml +49 -0
- package/src/core/tasks/shard-doc.xml +108 -0
- package/src/core/tasks/workflow.xml +235 -0
- package/src/core/workflows/advanced-elicitation/methods.csv +51 -0
- package/src/core/workflows/advanced-elicitation/workflow.xml +118 -0
- package/src/core/workflows/brainstorming/brain-methods.csv +62 -0
- package/src/core/workflows/brainstorming/steps/step-01-session-setup.md +197 -0
- package/src/core/workflows/brainstorming/steps/step-01b-continue.md +122 -0
- package/src/core/workflows/brainstorming/steps/step-02a-user-selected.md +225 -0
- package/src/core/workflows/brainstorming/steps/step-02b-ai-recommended.md +237 -0
- package/src/core/workflows/brainstorming/steps/step-02c-random-selection.md +209 -0
- package/src/core/workflows/brainstorming/steps/step-02d-progressive-flow.md +264 -0
- package/src/core/workflows/brainstorming/steps/step-03-technique-execution.md +399 -0
- package/src/core/workflows/brainstorming/steps/step-04-idea-organization.md +303 -0
- package/src/core/workflows/brainstorming/template.md +15 -0
- package/src/core/workflows/brainstorming/workflow.md +58 -0
- package/src/core/workflows/party-mode/steps/step-01-agent-loading.md +138 -0
- package/src/core/workflows/party-mode/steps/step-02-discussion-orchestration.md +187 -0
- package/src/core/workflows/party-mode/steps/step-03-graceful-exit.md +168 -0
- package/src/core/workflows/party-mode/workflow.md +194 -0
- package/src/utility/agent-components/activation-rules.txt +6 -0
- package/src/utility/agent-components/activation-steps.txt +14 -0
- package/src/utility/agent-components/agent-command-header.md +1 -0
- package/src/utility/agent-components/agent.customize.template.yaml +41 -0
- package/src/utility/agent-components/handler-action.txt +4 -0
- package/src/utility/agent-components/handler-data.txt +5 -0
- package/src/utility/agent-components/handler-exec.txt +6 -0
- package/src/utility/agent-components/handler-multi.txt +14 -0
- package/src/utility/agent-components/handler-tmpl.txt +5 -0
- package/src/utility/agent-components/handler-validate-workflow.txt +7 -0
- package/src/utility/agent-components/handler-workflow.txt +10 -0
- package/src/utility/agent-components/menu-handlers.txt +6 -0
- package/test/README.md +295 -0
- package/test/adversarial-review-tests/README.md +56 -0
- package/test/adversarial-review-tests/sample-content.md +46 -0
- package/test/adversarial-review-tests/test-cases.yaml +103 -0
- package/test/fixtures/agent-schema/invalid/critical-actions/actions-as-string.agent.yaml +27 -0
- package/test/fixtures/agent-schema/invalid/critical-actions/empty-string-in-actions.agent.yaml +30 -0
- package/test/fixtures/agent-schema/invalid/menu/empty-menu.agent.yaml +22 -0
- package/test/fixtures/agent-schema/invalid/menu/missing-menu.agent.yaml +20 -0
- package/test/fixtures/agent-schema/invalid/menu-commands/empty-command-target.agent.yaml +25 -0
- package/test/fixtures/agent-schema/invalid/menu-commands/no-command-target.agent.yaml +24 -0
- package/test/fixtures/agent-schema/invalid/menu-triggers/camel-case.agent.yaml +25 -0
- package/test/fixtures/agent-schema/invalid/menu-triggers/compound-invalid-format.agent.yaml +25 -0
- package/test/fixtures/agent-schema/invalid/menu-triggers/compound-mismatched-kebab.agent.yaml +25 -0
- package/test/fixtures/agent-schema/invalid/menu-triggers/duplicate-triggers.agent.yaml +31 -0
- package/test/fixtures/agent-schema/invalid/menu-triggers/empty-trigger.agent.yaml +25 -0
- package/test/fixtures/agent-schema/invalid/menu-triggers/leading-asterisk.agent.yaml +25 -0
- package/test/fixtures/agent-schema/invalid/menu-triggers/snake-case.agent.yaml +25 -0
- package/test/fixtures/agent-schema/invalid/menu-triggers/trigger-with-spaces.agent.yaml +25 -0
- package/test/fixtures/agent-schema/invalid/metadata/empty-module-string.agent.yaml +26 -0
- package/test/fixtures/agent-schema/invalid/metadata/empty-name.agent.yaml +24 -0
- package/test/fixtures/agent-schema/invalid/metadata/extra-metadata-fields.agent.yaml +27 -0
- package/test/fixtures/agent-schema/invalid/metadata/missing-id.agent.yaml +23 -0
- package/test/fixtures/agent-schema/invalid/persona/empty-principles-array.agent.yaml +24 -0
- package/test/fixtures/agent-schema/invalid/persona/empty-string-in-principles.agent.yaml +27 -0
- package/test/fixtures/agent-schema/invalid/persona/extra-persona-fields.agent.yaml +27 -0
- package/test/fixtures/agent-schema/invalid/persona/missing-role.agent.yaml +24 -0
- package/test/fixtures/agent-schema/invalid/prompts/empty-content.agent.yaml +29 -0
- package/test/fixtures/agent-schema/invalid/prompts/extra-prompt-fields.agent.yaml +31 -0
- package/test/fixtures/agent-schema/invalid/prompts/missing-content.agent.yaml +28 -0
- package/test/fixtures/agent-schema/invalid/prompts/missing-id.agent.yaml +28 -0
- package/test/fixtures/agent-schema/invalid/top-level/empty-file.agent.yaml +5 -0
- package/test/fixtures/agent-schema/invalid/top-level/extra-top-level-keys.agent.yaml +28 -0
- package/test/fixtures/agent-schema/invalid/top-level/missing-agent-key.agent.yaml +11 -0
- package/test/fixtures/agent-schema/invalid/yaml-errors/invalid-indentation.agent.yaml +19 -0
- package/test/fixtures/agent-schema/invalid/yaml-errors/malformed-yaml.agent.yaml +18 -0
- package/test/fixtures/agent-schema/valid/critical-actions/empty-critical-actions.agent.yaml +24 -0
- package/test/fixtures/agent-schema/valid/critical-actions/no-critical-actions.agent.yaml +22 -0
- package/test/fixtures/agent-schema/valid/critical-actions/valid-critical-actions.agent.yaml +27 -0
- package/test/fixtures/agent-schema/valid/menu/multiple-menu-items.agent.yaml +31 -0
- package/test/fixtures/agent-schema/valid/menu/single-menu-item.agent.yaml +22 -0
- package/test/fixtures/agent-schema/valid/menu-commands/all-command-types.agent.yaml +38 -0
- package/test/fixtures/agent-schema/valid/menu-commands/multiple-commands.agent.yaml +24 -0
- package/test/fixtures/agent-schema/valid/menu-triggers/compound-triggers.agent.yaml +31 -0
- package/test/fixtures/agent-schema/valid/menu-triggers/kebab-case-triggers.agent.yaml +34 -0
- package/test/fixtures/agent-schema/valid/metadata/core-agent-with-module.agent.yaml +24 -0
- package/test/fixtures/agent-schema/valid/metadata/empty-module-name-in-path.agent.yaml +24 -0
- package/test/fixtures/agent-schema/valid/metadata/malformed-path-treated-as-core.agent.yaml +24 -0
- package/test/fixtures/agent-schema/valid/metadata/module-agent-correct.agent.yaml +24 -0
- package/test/fixtures/agent-schema/valid/metadata/module-agent-missing-module.agent.yaml +23 -0
- package/test/fixtures/agent-schema/valid/metadata/wrong-module-value.agent.yaml +24 -0
- package/test/fixtures/agent-schema/valid/persona/complete-persona.agent.yaml +24 -0
- package/test/fixtures/agent-schema/valid/prompts/empty-prompts.agent.yaml +24 -0
- package/test/fixtures/agent-schema/valid/prompts/no-prompts.agent.yaml +22 -0
- package/test/fixtures/agent-schema/valid/prompts/valid-prompts-minimal.agent.yaml +28 -0
- package/test/fixtures/agent-schema/valid/prompts/valid-prompts-with-description.agent.yaml +30 -0
- package/test/fixtures/agent-schema/valid/top-level/minimal-core-agent.agent.yaml +24 -0
- package/test/fixtures/file-refs-csv/invalid/all-empty-workflow.csv +3 -0
- package/test/fixtures/file-refs-csv/invalid/empty-data.csv +1 -0
- package/test/fixtures/file-refs-csv/invalid/no-workflow-column.csv +3 -0
- package/test/fixtures/file-refs-csv/invalid/unresolvable-vars.csv +3 -0
- package/test/fixtures/file-refs-csv/valid/bmm-style.csv +3 -0
- package/test/fixtures/file-refs-csv/valid/core-style.csv +3 -0
- package/test/fixtures/file-refs-csv/valid/minimal.csv +2 -0
- package/test/test-agent-schema.js +387 -0
- package/test/test-cli-integration.sh +159 -0
- package/test/test-file-refs-csv.js +133 -0
- package/test/test-installation-components.js +212 -0
- package/test/test-rehype-plugins.mjs +1050 -0
- package/test/unit-test-schema.js +133 -0
- package/tests/run_all_tests.py +80 -0
- package/tests/scenarios/cis/brainstorming-coach.test.py +150 -0
- package/tests/scenarios/cis/creative-problem-solver.test.py +167 -0
- package/tests/scenarios/cis/design-thinking-coach.test.py +177 -0
- package/tests/scenarios/cis/innovation-strategist.test.py +191 -0
- package/tests/scenarios/cis/presentation-master.test.py +240 -0
- package/tests/scenarios/cis/storyteller.test.py +324 -0
- package/tests/scenarios/core/mdan-master.test.py +281 -0
- package/tests/scenarios/mmb/agent-builder.test.py +124 -0
- package/tests/scenarios/mmb/module-builder.test.py +124 -0
- package/tests/scenarios/mmb/workflow-builder.test.py +124 -0
- package/tests/scenarios/mmm/analyst.test.py +138 -0
- package/tests/scenarios/mmm/architect.test.py +138 -0
- package/tests/scenarios/mmm/dev.test.py +138 -0
- package/tests/scenarios/mmm/pm.test.py +138 -0
- package/tests/scenarios/mmm/qa.test.py +138 -0
- package/tests/scenarios/mmm/quick-flow-solo-dev.test.py +138 -0
- package/tests/scenarios/mmm/sm.test.py +138 -0
- package/tests/scenarios/mmm/tech-writer.test.py +138 -0
- package/tests/scenarios/mmm/ux-designer.test.py +294 -0
- package/tests/scenarios/packs/db-optimization/db-performance-analyst.test.py +108 -0
- package/tests/scenarios/packs/db-optimization/indexing-specialist.test.py +108 -0
- package/tests/scenarios/packs/db-optimization/query-optimizer.test.py +106 -0
- package/tests/scenarios/packs/devops-azure/azure-specialist.test.py +125 -0
- package/tests/scenarios/packs/devops-azure/cicd-architect.test.py +122 -0
- package/tests/scenarios/packs/devops-azure/devops-engineer.test.py +128 -0
- package/tests/scenarios/packs/fintech/compliance-officer.test.py +165 -0
- package/tests/scenarios/packs/fintech/financial-analyst.test.py +184 -0
- package/tests/scenarios/packs/fintech/risk-manager.test.py +171 -0
- package/tests/scenarios/tea/tea.test.py +346 -0
- package/tests/simple_cis_test.py +285 -0
- package/tests/simple_db_optimization_test.py +199 -0
- package/tests/simple_devops_test.py +193 -0
- package/tests/simple_fintech_test.py +205 -0
- package/tests/simple_mmb_test.py +103 -0
- package/tests/simple_mmm_test.py +159 -0
- package/tests/simple_tea_test.py +80 -0
- package/tests/simple_test.py +111 -0
- package/tests/simple_ux_designer_test.py +144 -0
- package/tests/validate_yaml.py +86 -0
- package/tools/bmad-npx-wrapper.js +38 -0
- package/tools/build-docs.mjs +463 -0
- package/tools/cli/README.md +60 -0
- package/tools/cli/bmad-cli.js +106 -0
- package/tools/cli/commands/install.js +87 -0
- package/tools/cli/commands/status.js +65 -0
- package/tools/cli/commands/uninstall.js +167 -0
- package/tools/cli/external-official-modules.yaml +53 -0
- package/tools/cli/installers/install-messages.yaml +39 -0
- package/tools/cli/installers/lib/core/config-collector.js +1285 -0
- package/tools/cli/installers/lib/core/custom-module-cache.js +260 -0
- package/tools/cli/installers/lib/core/dependency-resolver.js +743 -0
- package/tools/cli/installers/lib/core/detector.js +223 -0
- package/tools/cli/installers/lib/core/ide-config-manager.js +157 -0
- package/tools/cli/installers/lib/core/installer.js +3162 -0
- package/tools/cli/installers/lib/core/manifest-generator.js +1081 -0
- package/tools/cli/installers/lib/core/manifest.js +1038 -0
- package/tools/cli/installers/lib/custom/handler.js +358 -0
- package/tools/cli/installers/lib/ide/_base-ide.js +665 -0
- package/tools/cli/installers/lib/ide/_config-driven.js +634 -0
- package/tools/cli/installers/lib/ide/codex.js +440 -0
- package/tools/cli/installers/lib/ide/github-copilot.js +699 -0
- package/tools/cli/installers/lib/ide/kilo.js +269 -0
- package/tools/cli/installers/lib/ide/manager.js +342 -0
- package/tools/cli/installers/lib/ide/platform-codes.js +100 -0
- package/tools/cli/installers/lib/ide/platform-codes.yaml +243 -0
- package/tools/cli/installers/lib/ide/rovodev.js +257 -0
- package/tools/cli/installers/lib/ide/shared/agent-command-generator.js +180 -0
- package/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +174 -0
- package/tools/cli/installers/lib/ide/shared/module-injections.js +136 -0
- package/tools/cli/installers/lib/ide/shared/path-utils.js +299 -0
- package/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +366 -0
- package/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +318 -0
- package/tools/cli/installers/lib/ide/templates/agent-command-template.md +14 -0
- package/tools/cli/installers/lib/ide/templates/combined/antigravity.md +8 -0
- package/tools/cli/installers/lib/ide/templates/combined/default-agent.md +15 -0
- package/tools/cli/installers/lib/ide/templates/combined/default-task.md +10 -0
- package/tools/cli/installers/lib/ide/templates/combined/default-tool.md +10 -0
- package/tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md +14 -0
- package/tools/cli/installers/lib/ide/templates/combined/default-workflow.md +6 -0
- package/tools/cli/installers/lib/ide/templates/combined/gemini-agent.toml +14 -0
- package/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml +11 -0
- package/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml +11 -0
- package/tools/cli/installers/lib/ide/templates/combined/gemini-workflow-yaml.toml +16 -0
- package/tools/cli/installers/lib/ide/templates/combined/gemini-workflow.toml +14 -0
- package/tools/cli/installers/lib/ide/templates/combined/kiro-agent.md +16 -0
- package/tools/cli/installers/lib/ide/templates/combined/kiro-task.md +9 -0
- package/tools/cli/installers/lib/ide/templates/combined/kiro-tool.md +9 -0
- package/tools/cli/installers/lib/ide/templates/combined/kiro-workflow-yaml.md +15 -0
- package/tools/cli/installers/lib/ide/templates/combined/kiro-workflow.md +7 -0
- package/tools/cli/installers/lib/ide/templates/combined/opencode-agent.md +15 -0
- package/tools/cli/installers/lib/ide/templates/combined/opencode-task.md +13 -0
- package/tools/cli/installers/lib/ide/templates/combined/opencode-tool.md +13 -0
- package/tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md +16 -0
- package/tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md +16 -0
- package/tools/cli/installers/lib/ide/templates/combined/rovodev.md +9 -0
- package/tools/cli/installers/lib/ide/templates/combined/trae.md +9 -0
- package/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md +10 -0
- package/tools/cli/installers/lib/ide/templates/split/.gitkeep +0 -0
- package/tools/cli/installers/lib/ide/templates/workflow-command-template.md +13 -0
- package/tools/cli/installers/lib/ide/templates/workflow-commander.md +5 -0
- package/tools/cli/installers/lib/message-loader.js +83 -0
- package/tools/cli/installers/lib/modules/external-manager.js +136 -0
- package/tools/cli/installers/lib/modules/manager.js +1498 -0
- package/tools/cli/lib/activation-builder.js +165 -0
- package/tools/cli/lib/agent/compiler.js +525 -0
- package/tools/cli/lib/agent/installer.js +680 -0
- package/tools/cli/lib/agent/template-engine.js +152 -0
- package/tools/cli/lib/agent-analyzer.js +109 -0
- package/tools/cli/lib/agent-party-generator.js +194 -0
- package/tools/cli/lib/cli-utils.js +182 -0
- package/tools/cli/lib/config.js +213 -0
- package/tools/cli/lib/file-ops.js +204 -0
- package/tools/cli/lib/platform-codes.js +116 -0
- package/tools/cli/lib/project-root.js +77 -0
- package/tools/cli/lib/prompts.js +809 -0
- package/tools/cli/lib/ui.js +1936 -0
- package/tools/cli/lib/xml-handler.js +177 -0
- package/tools/cli/lib/xml-to-markdown.js +82 -0
- package/tools/cli/lib/yaml-format.js +245 -0
- package/tools/cli/lib/yaml-xml-builder.js +587 -0
- package/tools/docs/_prompt-external-modules-page.md +59 -0
- package/tools/docs/fix-refs.md +91 -0
- package/tools/fix-doc-links.js +285 -0
- package/tools/format-workflow-md.js +263 -0
- package/tools/lib/xml-utils.js +13 -0
- package/tools/migrate-custom-module-paths.js +124 -0
- package/tools/platform-codes.yaml +157 -0
- package/tools/schema/agent.js +491 -0
- package/tools/validate-agent-schema.js +110 -0
- package/tools/validate-doc-links.js +407 -0
- package/tools/validate-file-refs.js +554 -0
- package/tools/validate-svg-changes.sh +356 -0
- package/website/README.md +75 -0
- package/website/astro.config.mjs +136 -0
- package/website/public/favicon.ico +0 -0
- package/website/public/img/bmad-dark.png +0 -0
- package/website/public/img/bmad-light.png +0 -0
- package/website/public/workflow-map-diagram.html +361 -0
- package/website/src/components/Banner.astro +62 -0
- package/website/src/components/Header.astro +96 -0
- package/website/src/components/MobileMenuFooter.astro +33 -0
- package/website/src/content/config.ts +6 -0
- package/website/src/lib/site-url.mjs +25 -0
- package/website/src/pages/404.astro +11 -0
- package/website/src/pages/robots.txt.ts +48 -0
- package/website/src/rehype-base-paths.js +112 -0
- package/website/src/rehype-markdown-links.js +119 -0
- package/website/src/styles/custom.css +805 -0
- package/.mcp.json +0 -46
- package/agents/AGENTS-REGISTRY.md +0 -215
- package/agents/architect.md +0 -160
- package/agents/dev.md +0 -166
- package/agents/devops.md +0 -230
- package/agents/doc.md +0 -189
- package/agents/learn.md +0 -377
- package/agents/product.md +0 -124
- package/agents/security.md +0 -168
- package/agents/test.md +0 -209
- package/agents/ux.md +0 -207
- package/cli/mdan.js +0 -628
- package/cli/mdan.py +0 -316
- package/cli/mdan.sh +0 -724
- package/cli/postinstall.js +0 -4
- package/core/orchestrator.md +0 -238
- package/core/universal-envelope.md +0 -160
- package/install.sh +0 -91
- package/integrations/all-integrations.md +0 -300
- package/integrations/claude.md +0 -46
- package/integrations/cursor.md +0 -74
- package/integrations/mcp.md +0 -153
- package/integrations/windsurf.md +0 -48
- package/memory/MDAN-STATE.template.json +0 -44
- package/memory/MEMORY-SYSTEM.md +0 -197
- package/phases/01-discover.md +0 -136
- package/phases/02-design.md +0 -147
- package/phases/03-build.md +0 -113
- package/phases/04-verify.md +0 -107
- package/phases/05-ship.md +0 -156
- package/skills/find-skills/skill.md +0 -133
- package/templates/ARCHITECTURE.md +0 -186
- package/templates/CHANGELOG.md +0 -41
- package/templates/MDAN-KNOWLEDGE.md +0 -73
- package/templates/PRD.md +0 -120
- package/templates/SECURITY-REVIEW.md +0 -99
- package/templates/TEST-PLAN.md +0 -97
- package/templates/prompts/README.md +0 -108
- package/templates/prompts/dev-agent.yaml +0 -85
- package/templates/prompts/orchestrator.yaml +0 -97
- package/templates/prompts.json +0 -81
- package/templates/tests/evaluations/README.md +0 -80
- package/templates/tests/evaluations/classification_eval.md +0 -136
- package/templates/tests/evaluations/rag_eval.md +0 -116
- package/templates/tests/scenarios/README.md +0 -62
- package/templates/tests/scenarios/basic_authentication.test.md +0 -82
- package/templates/tests/scenarios/user_registration.test.md +0 -107
|
@@ -0,0 +1,3162 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { Detector } = require('./detector');
|
|
4
|
+
const { Manifest } = require('./manifest');
|
|
5
|
+
const { ModuleManager } = require('../modules/manager');
|
|
6
|
+
const { IdeManager } = require('../ide/manager');
|
|
7
|
+
const { FileOps } = require('../../../lib/file-ops');
|
|
8
|
+
const { Config } = require('../../../lib/config');
|
|
9
|
+
const { XmlHandler } = require('../../../lib/xml-handler');
|
|
10
|
+
const { DependencyResolver } = require('./dependency-resolver');
|
|
11
|
+
const { ConfigCollector } = require('./config-collector');
|
|
12
|
+
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
|
13
|
+
const { CLIUtils } = require('../../../lib/cli-utils');
|
|
14
|
+
const { ManifestGenerator } = require('./manifest-generator');
|
|
15
|
+
const { IdeConfigManager } = require('./ide-config-manager');
|
|
16
|
+
const { CustomHandler } = require('../custom/handler');
|
|
17
|
+
const prompts = require('../../../lib/prompts');
|
|
18
|
+
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
|
19
|
+
|
|
20
|
+
class Installer {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.detector = new Detector();
|
|
23
|
+
this.manifest = new Manifest();
|
|
24
|
+
this.moduleManager = new ModuleManager();
|
|
25
|
+
this.ideManager = new IdeManager();
|
|
26
|
+
this.fileOps = new FileOps();
|
|
27
|
+
this.config = new Config();
|
|
28
|
+
this.xmlHandler = new XmlHandler();
|
|
29
|
+
this.dependencyResolver = new DependencyResolver();
|
|
30
|
+
this.configCollector = new ConfigCollector();
|
|
31
|
+
this.ideConfigManager = new IdeConfigManager();
|
|
32
|
+
this.installedFiles = new Set(); // Track all installed files
|
|
33
|
+
this.bmadFolderName = BMAD_FOLDER_NAME;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Find the bmad installation directory in a project
|
|
38
|
+
* Always uses the standard _bmad folder name
|
|
39
|
+
* Also checks for legacy _cfg folder for migration
|
|
40
|
+
* @param {string} projectDir - Project directory
|
|
41
|
+
* @returns {Promise<Object>} { bmadDir: string, hasLegacyCfg: boolean }
|
|
42
|
+
*/
|
|
43
|
+
async findBmadDir(projectDir) {
|
|
44
|
+
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
|
45
|
+
|
|
46
|
+
// Check if project directory exists
|
|
47
|
+
if (!(await fs.pathExists(projectDir))) {
|
|
48
|
+
// Project doesn't exist yet, return default
|
|
49
|
+
return { bmadDir, hasLegacyCfg: false };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check for legacy _cfg folder if bmad directory exists
|
|
53
|
+
let hasLegacyCfg = false;
|
|
54
|
+
if (await fs.pathExists(bmadDir)) {
|
|
55
|
+
const legacyCfgPath = path.join(bmadDir, '_cfg');
|
|
56
|
+
if (await fs.pathExists(legacyCfgPath)) {
|
|
57
|
+
hasLegacyCfg = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { bmadDir, hasLegacyCfg };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @function copyFileWithPlaceholderReplacement
|
|
66
|
+
* @intent Copy files from BMAD source to installation directory with dynamic content transformation
|
|
67
|
+
* @why Enables installation-time customization: _bmad replacement
|
|
68
|
+
* @param {string} sourcePath - Absolute path to source file in BMAD repository
|
|
69
|
+
* @param {string} targetPath - Absolute path to destination file in user's project
|
|
70
|
+
* @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
|
|
71
|
+
* @returns {Promise<void>} Resolves when file copy and transformation complete
|
|
72
|
+
* @sideeffects Writes transformed file to targetPath, creates parent directories if needed
|
|
73
|
+
* @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails
|
|
74
|
+
* @calledby installCore(), installModule(), IDE installers during file vendoring
|
|
75
|
+
* @calls fs.readFile(), fs.writeFile(), fs.copy()
|
|
76
|
+
*
|
|
77
|
+
|
|
78
|
+
*
|
|
79
|
+
* 3. Document marker in instructions.md (if applicable)
|
|
80
|
+
*/
|
|
81
|
+
async copyFileWithPlaceholderReplacement(sourcePath, targetPath) {
|
|
82
|
+
// List of text file extensions that should have placeholder replacement
|
|
83
|
+
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
|
|
84
|
+
const ext = path.extname(sourcePath).toLowerCase();
|
|
85
|
+
|
|
86
|
+
// Check if this is a text file that might contain placeholders
|
|
87
|
+
if (textExtensions.includes(ext)) {
|
|
88
|
+
try {
|
|
89
|
+
// Read the file content
|
|
90
|
+
let content = await fs.readFile(sourcePath, 'utf8');
|
|
91
|
+
|
|
92
|
+
// Write to target with replaced content
|
|
93
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
94
|
+
await fs.writeFile(targetPath, content, 'utf8');
|
|
95
|
+
} catch {
|
|
96
|
+
// If reading as text fails (might be binary despite extension), fall back to regular copy
|
|
97
|
+
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// Binary file or other file type - just copy directly
|
|
101
|
+
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Collect Tool/IDE configurations after module configuration
|
|
107
|
+
* @param {string} projectDir - Project directory
|
|
108
|
+
* @param {Array} selectedModules - Selected modules from configuration
|
|
109
|
+
* @param {boolean} isFullReinstall - Whether this is a full reinstall
|
|
110
|
+
* @param {Array} previousIdes - Previously configured IDEs (for reinstalls)
|
|
111
|
+
* @param {Array} preSelectedIdes - Pre-selected IDEs from early prompt (optional)
|
|
112
|
+
* @param {boolean} skipPrompts - Skip prompts and use defaults (for --yes flag)
|
|
113
|
+
* @returns {Object} Tool/IDE selection and configurations
|
|
114
|
+
*/
|
|
115
|
+
async collectToolConfigurations(
|
|
116
|
+
projectDir,
|
|
117
|
+
selectedModules,
|
|
118
|
+
isFullReinstall = false,
|
|
119
|
+
previousIdes = [],
|
|
120
|
+
preSelectedIdes = null,
|
|
121
|
+
skipPrompts = false,
|
|
122
|
+
) {
|
|
123
|
+
// Use pre-selected IDEs if provided, otherwise prompt
|
|
124
|
+
let toolConfig;
|
|
125
|
+
if (preSelectedIdes === null) {
|
|
126
|
+
// Fallback: prompt for tool selection (backwards compatibility)
|
|
127
|
+
const { UI } = require('../../../lib/ui');
|
|
128
|
+
const ui = new UI();
|
|
129
|
+
toolConfig = await ui.promptToolSelection(projectDir);
|
|
130
|
+
} else {
|
|
131
|
+
// IDEs were already selected during initial prompts
|
|
132
|
+
toolConfig = {
|
|
133
|
+
ides: preSelectedIdes,
|
|
134
|
+
skipIde: !preSelectedIdes || preSelectedIdes.length === 0,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for already configured IDEs
|
|
139
|
+
const { Detector } = require('./detector');
|
|
140
|
+
const detector = new Detector();
|
|
141
|
+
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
|
142
|
+
|
|
143
|
+
// During full reinstall, use the saved previous IDEs since bmad dir was deleted
|
|
144
|
+
// Otherwise detect from existing installation
|
|
145
|
+
let previouslyConfiguredIdes;
|
|
146
|
+
if (isFullReinstall) {
|
|
147
|
+
// During reinstall, treat all IDEs as new (need configuration)
|
|
148
|
+
previouslyConfiguredIdes = [];
|
|
149
|
+
} else {
|
|
150
|
+
const existingInstall = await detector.detect(bmadDir);
|
|
151
|
+
previouslyConfiguredIdes = existingInstall.ides || [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Load saved IDE configurations for already-configured IDEs
|
|
155
|
+
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
|
156
|
+
|
|
157
|
+
// Collect IDE-specific configurations if any were selected
|
|
158
|
+
const ideConfigurations = {};
|
|
159
|
+
|
|
160
|
+
// First, add saved configs for already-configured IDEs
|
|
161
|
+
for (const ide of toolConfig.ides || []) {
|
|
162
|
+
if (previouslyConfiguredIdes.includes(ide) && savedIdeConfigs[ide]) {
|
|
163
|
+
ideConfigurations[ide] = savedIdeConfigs[ide];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
|
|
168
|
+
// Ensure IDE manager is initialized
|
|
169
|
+
await this.ideManager.ensureInitialized();
|
|
170
|
+
|
|
171
|
+
// Determine which IDEs are newly selected (not previously configured)
|
|
172
|
+
const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide));
|
|
173
|
+
|
|
174
|
+
if (newlySelectedIdes.length > 0) {
|
|
175
|
+
// Collect configuration for IDEs that support it
|
|
176
|
+
for (const ide of newlySelectedIdes) {
|
|
177
|
+
try {
|
|
178
|
+
const handler = this.ideManager.handlers.get(ide);
|
|
179
|
+
|
|
180
|
+
if (!handler) {
|
|
181
|
+
await prompts.log.warn(`Warning: IDE '${ide}' handler not found`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check if this IDE handler has a collectConfiguration method
|
|
186
|
+
// (custom installers like Codex, Kilo may have this)
|
|
187
|
+
if (typeof handler.collectConfiguration === 'function') {
|
|
188
|
+
await prompts.log.info(`Configuring ${ide}...`);
|
|
189
|
+
ideConfigurations[ide] = await handler.collectConfiguration({
|
|
190
|
+
selectedModules: selectedModules || [],
|
|
191
|
+
projectDir,
|
|
192
|
+
bmadDir,
|
|
193
|
+
skipPrompts,
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
// Config-driven IDEs don't need configuration - mark as ready
|
|
197
|
+
ideConfigurations[ide] = { _noConfigNeeded: true };
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// IDE doesn't support configuration or has an error
|
|
201
|
+
await prompts.log.warn(`Warning: Could not load configuration for ${ide}: ${error.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Log which IDEs are already configured and being kept
|
|
207
|
+
const keptIdes = toolConfig.ides.filter((ide) => previouslyConfiguredIdes.includes(ide));
|
|
208
|
+
if (keptIdes.length > 0) {
|
|
209
|
+
await prompts.log.message(`Keeping existing configuration for: ${keptIdes.join(', ')}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
ides: toolConfig.ides,
|
|
215
|
+
skipIde: toolConfig.skipIde,
|
|
216
|
+
configurations: ideConfigurations,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Main installation method
|
|
222
|
+
* @param {Object} config - Installation configuration
|
|
223
|
+
* @param {string} config.directory - Target directory
|
|
224
|
+
* @param {boolean} config.installCore - Whether to install core
|
|
225
|
+
* @param {string[]} config.modules - Modules to install
|
|
226
|
+
* @param {string[]} config.ides - IDEs to configure
|
|
227
|
+
* @param {boolean} config.skipIde - Skip IDE configuration
|
|
228
|
+
*/
|
|
229
|
+
async install(originalConfig) {
|
|
230
|
+
// Clone config to avoid mutating the caller's object
|
|
231
|
+
const config = { ...originalConfig };
|
|
232
|
+
|
|
233
|
+
// Check if core config was already collected in UI
|
|
234
|
+
const hasCoreConfig = config.coreConfig && Object.keys(config.coreConfig).length > 0;
|
|
235
|
+
|
|
236
|
+
// Only display logo if core config wasn't already collected (meaning we're not continuing from UI)
|
|
237
|
+
if (!hasCoreConfig) {
|
|
238
|
+
// Display BMAD logo
|
|
239
|
+
await CLIUtils.displayLogo();
|
|
240
|
+
|
|
241
|
+
// Display welcome message
|
|
242
|
+
await CLIUtils.displaySection('BMad™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Note: Legacy V4 detection now happens earlier in UI.promptInstall()
|
|
246
|
+
// before any config collection, so we don't need to check again here
|
|
247
|
+
|
|
248
|
+
const projectDir = path.resolve(config.directory);
|
|
249
|
+
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
|
250
|
+
|
|
251
|
+
// If core config was pre-collected (from interactive mode), use it
|
|
252
|
+
if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
|
|
253
|
+
this.configCollector.collectedConfig.core = config.coreConfig;
|
|
254
|
+
// Also store in allAnswers for cross-referencing
|
|
255
|
+
this.configCollector.allAnswers = {};
|
|
256
|
+
for (const [key, value] of Object.entries(config.coreConfig)) {
|
|
257
|
+
this.configCollector.allAnswers[`core_${key}`] = value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Collect configurations for modules (skip if quick update already collected them)
|
|
262
|
+
let moduleConfigs;
|
|
263
|
+
let customModulePaths = new Map();
|
|
264
|
+
|
|
265
|
+
if (config._quickUpdate) {
|
|
266
|
+
// Quick update already collected all configs, use them directly
|
|
267
|
+
moduleConfigs = this.configCollector.collectedConfig;
|
|
268
|
+
|
|
269
|
+
// For quick update, populate customModulePaths from _customModuleSources
|
|
270
|
+
if (config._customModuleSources) {
|
|
271
|
+
for (const [moduleId, customInfo] of config._customModuleSources) {
|
|
272
|
+
customModulePaths.set(moduleId, customInfo.sourcePath);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// For regular updates (modify flow), check manifest for custom module sources
|
|
277
|
+
if (config._isUpdate && config._existingInstall && config._existingInstall.customModules) {
|
|
278
|
+
for (const customModule of config._existingInstall.customModules) {
|
|
279
|
+
// Ensure we have an absolute sourcePath
|
|
280
|
+
let absoluteSourcePath = customModule.sourcePath;
|
|
281
|
+
|
|
282
|
+
// Check if sourcePath is a cache-relative path (starts with _config)
|
|
283
|
+
if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
|
|
284
|
+
// Convert cache-relative path to absolute path
|
|
285
|
+
absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
|
|
286
|
+
}
|
|
287
|
+
// If no sourcePath but we have relativePath, convert it
|
|
288
|
+
else if (!absoluteSourcePath && customModule.relativePath) {
|
|
289
|
+
// relativePath is relative to the project root (parent of bmad dir)
|
|
290
|
+
absoluteSourcePath = path.resolve(projectDir, customModule.relativePath);
|
|
291
|
+
}
|
|
292
|
+
// Ensure sourcePath is absolute for anything else
|
|
293
|
+
else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
|
|
294
|
+
absoluteSourcePath = path.resolve(absoluteSourcePath);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (absoluteSourcePath) {
|
|
298
|
+
customModulePaths.set(customModule.id, absoluteSourcePath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Build custom module paths map from customContent
|
|
304
|
+
|
|
305
|
+
// Handle selectedFiles (from existing install path or manual directory input)
|
|
306
|
+
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
|
|
307
|
+
const customHandler = new CustomHandler();
|
|
308
|
+
for (const customFile of config.customContent.selectedFiles) {
|
|
309
|
+
const customInfo = await customHandler.getCustomInfo(customFile, path.resolve(config.directory));
|
|
310
|
+
if (customInfo && customInfo.id) {
|
|
311
|
+
customModulePaths.set(customInfo.id, customInfo.path);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Handle new custom content sources from UI
|
|
317
|
+
if (config.customContent && config.customContent.sources) {
|
|
318
|
+
for (const source of config.customContent.sources) {
|
|
319
|
+
customModulePaths.set(source.id, source.path);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Handle cachedModules (from new install path where modules are cached)
|
|
324
|
+
// Only include modules that were actually selected for installation
|
|
325
|
+
if (config.customContent && config.customContent.cachedModules) {
|
|
326
|
+
// Get selected cached module IDs (if available)
|
|
327
|
+
const selectedCachedIds = config.customContent.selectedCachedModules || [];
|
|
328
|
+
// If no selection info, include all cached modules (for backward compatibility)
|
|
329
|
+
const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected;
|
|
330
|
+
|
|
331
|
+
for (const cachedModule of config.customContent.cachedModules) {
|
|
332
|
+
// For cached modules, the path is the cachePath which contains the module.yaml
|
|
333
|
+
if (
|
|
334
|
+
cachedModule.id &&
|
|
335
|
+
cachedModule.cachePath && // Include if selected or if we should include all
|
|
336
|
+
(shouldIncludeAll || selectedCachedIds.includes(cachedModule.id))
|
|
337
|
+
) {
|
|
338
|
+
customModulePaths.set(cachedModule.id, cachedModule.cachePath);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Get list of all modules including custom modules
|
|
344
|
+
// Order: core first, then official modules, then custom modules
|
|
345
|
+
const allModulesForConfig = ['core'];
|
|
346
|
+
|
|
347
|
+
// Add official modules (excluding core and any custom modules)
|
|
348
|
+
const officialModules = (config.modules || []).filter((m) => m !== 'core' && !customModulePaths.has(m));
|
|
349
|
+
allModulesForConfig.push(...officialModules);
|
|
350
|
+
|
|
351
|
+
// Add custom modules at the end
|
|
352
|
+
for (const [moduleId] of customModulePaths) {
|
|
353
|
+
if (!allModulesForConfig.includes(moduleId)) {
|
|
354
|
+
allModulesForConfig.push(moduleId);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Check if core was already collected in UI
|
|
359
|
+
if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
|
|
360
|
+
// Core already collected, skip it in config collection
|
|
361
|
+
const modulesWithoutCore = allModulesForConfig.filter((m) => m !== 'core');
|
|
362
|
+
moduleConfigs = await this.configCollector.collectAllConfigurations(modulesWithoutCore, path.resolve(config.directory), {
|
|
363
|
+
customModulePaths,
|
|
364
|
+
skipPrompts: config.skipPrompts,
|
|
365
|
+
});
|
|
366
|
+
} else {
|
|
367
|
+
// Core not collected yet, include it
|
|
368
|
+
moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), {
|
|
369
|
+
customModulePaths,
|
|
370
|
+
skipPrompts: config.skipPrompts,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Set bmad folder name on module manager and IDE manager for placeholder replacement
|
|
376
|
+
this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
|
377
|
+
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
|
|
378
|
+
this.moduleManager.setCustomModulePaths(customModulePaths);
|
|
379
|
+
this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
|
380
|
+
|
|
381
|
+
// Tool selection will be collected after we determine if it's a reinstall/update/new install
|
|
382
|
+
|
|
383
|
+
const spinner = await prompts.spinner();
|
|
384
|
+
spinner.start('Preparing installation...');
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
// Create a project directory if it doesn't exist (user already confirmed)
|
|
388
|
+
if (!(await fs.pathExists(projectDir))) {
|
|
389
|
+
spinner.message('Creating installation directory...');
|
|
390
|
+
try {
|
|
391
|
+
// fs.ensureDir handles platform-specific directory creation
|
|
392
|
+
// It will recursively create all necessary parent directories
|
|
393
|
+
await fs.ensureDir(projectDir);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
spinner.error('Failed to create installation directory');
|
|
396
|
+
await prompts.log.error(`Error: ${error.message}`);
|
|
397
|
+
// More detailed error for common issues
|
|
398
|
+
if (error.code === 'EACCES') {
|
|
399
|
+
await prompts.log.error('Permission denied. Check parent directory permissions.');
|
|
400
|
+
} else if (error.code === 'ENOSPC') {
|
|
401
|
+
await prompts.log.error('No space left on device.');
|
|
402
|
+
}
|
|
403
|
+
throw new Error(`Cannot create directory: ${projectDir}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Check existing installation
|
|
408
|
+
spinner.message('Checking for existing installation...');
|
|
409
|
+
const existingInstall = await this.detector.detect(bmadDir);
|
|
410
|
+
|
|
411
|
+
if (existingInstall.installed && !config.force && !config._quickUpdate) {
|
|
412
|
+
spinner.stop('Existing installation detected');
|
|
413
|
+
|
|
414
|
+
// Check if user already decided what to do (from early menu in ui.js)
|
|
415
|
+
let action = null;
|
|
416
|
+
if (config.actionType === 'update') {
|
|
417
|
+
action = 'update';
|
|
418
|
+
} else if (config.skipPrompts) {
|
|
419
|
+
// Non-interactive mode: default to update
|
|
420
|
+
action = 'update';
|
|
421
|
+
} else {
|
|
422
|
+
// Fallback: Ask the user (backwards compatibility for other code paths)
|
|
423
|
+
await prompts.log.warn('Existing BMAD installation detected');
|
|
424
|
+
await prompts.log.message(` Location: ${bmadDir}`);
|
|
425
|
+
await prompts.log.message(` Version: ${existingInstall.version}`);
|
|
426
|
+
|
|
427
|
+
const promptResult = await this.promptUpdateAction();
|
|
428
|
+
action = promptResult.action;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (action === 'update') {
|
|
432
|
+
// Store that we're updating for later processing
|
|
433
|
+
config._isUpdate = true;
|
|
434
|
+
config._existingInstall = existingInstall;
|
|
435
|
+
|
|
436
|
+
// Detect modules that were previously installed but are NOT in the new selection (to be removed)
|
|
437
|
+
const previouslyInstalledModules = new Set(existingInstall.modules.map((m) => m.id));
|
|
438
|
+
const newlySelectedModules = new Set(config.modules || []);
|
|
439
|
+
|
|
440
|
+
// Find modules to remove (installed but not in new selection)
|
|
441
|
+
// Exclude 'core' from being removable
|
|
442
|
+
const modulesToRemove = [...previouslyInstalledModules].filter((m) => !newlySelectedModules.has(m) && m !== 'core');
|
|
443
|
+
|
|
444
|
+
// If there are modules to remove, ask for confirmation
|
|
445
|
+
if (modulesToRemove.length > 0) {
|
|
446
|
+
if (config.skipPrompts) {
|
|
447
|
+
// Non-interactive mode: preserve modules (matches prompt default: false)
|
|
448
|
+
for (const moduleId of modulesToRemove) {
|
|
449
|
+
if (!config.modules) config.modules = [];
|
|
450
|
+
config.modules.push(moduleId);
|
|
451
|
+
}
|
|
452
|
+
spinner.start('Preparing update...');
|
|
453
|
+
} else {
|
|
454
|
+
if (spinner.isSpinning) {
|
|
455
|
+
spinner.stop('Module changes reviewed');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
await prompts.log.warn('Modules to be removed:');
|
|
459
|
+
for (const moduleId of modulesToRemove) {
|
|
460
|
+
const moduleInfo = existingInstall.modules.find((m) => m.id === moduleId);
|
|
461
|
+
const displayName = moduleInfo?.name || moduleId;
|
|
462
|
+
const modulePath = path.join(bmadDir, moduleId);
|
|
463
|
+
await prompts.log.error(` - ${displayName} (${modulePath})`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const confirmRemoval = await prompts.confirm({
|
|
467
|
+
message: `Remove ${modulesToRemove.length} module(s) from BMAD installation?`,
|
|
468
|
+
default: false,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
if (confirmRemoval) {
|
|
472
|
+
// Remove module folders
|
|
473
|
+
for (const moduleId of modulesToRemove) {
|
|
474
|
+
const modulePath = path.join(bmadDir, moduleId);
|
|
475
|
+
try {
|
|
476
|
+
if (await fs.pathExists(modulePath)) {
|
|
477
|
+
await fs.remove(modulePath);
|
|
478
|
+
await prompts.log.message(` Removed: ${moduleId}`);
|
|
479
|
+
}
|
|
480
|
+
} catch (error) {
|
|
481
|
+
await prompts.log.warn(` Warning: Failed to remove ${moduleId}: ${error.message}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
await prompts.log.success(` Removed ${modulesToRemove.length} module(s)`);
|
|
485
|
+
} else {
|
|
486
|
+
await prompts.log.message(' Module removal cancelled');
|
|
487
|
+
// Add the modules back to the selection since user cancelled removal
|
|
488
|
+
for (const moduleId of modulesToRemove) {
|
|
489
|
+
if (!config.modules) config.modules = [];
|
|
490
|
+
config.modules.push(moduleId);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
spinner.start('Preparing update...');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
|
|
499
|
+
const existingFilesManifest = await this.readFilesManifest(bmadDir);
|
|
500
|
+
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
|
|
501
|
+
|
|
502
|
+
config._customFiles = customFiles;
|
|
503
|
+
config._modifiedFiles = modifiedFiles;
|
|
504
|
+
|
|
505
|
+
// Preserve existing core configuration during updates
|
|
506
|
+
// Read the current core config.yaml to maintain user's settings
|
|
507
|
+
const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
|
|
508
|
+
if ((await fs.pathExists(coreConfigPath)) && (!config.coreConfig || Object.keys(config.coreConfig).length === 0)) {
|
|
509
|
+
try {
|
|
510
|
+
const yaml = require('yaml');
|
|
511
|
+
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
|
|
512
|
+
const existingCoreConfig = yaml.parse(coreConfigContent);
|
|
513
|
+
|
|
514
|
+
// Store in config.coreConfig so it's preserved through the installation
|
|
515
|
+
config.coreConfig = existingCoreConfig;
|
|
516
|
+
|
|
517
|
+
// Also store in configCollector for use during config collection
|
|
518
|
+
this.configCollector.collectedConfig.core = existingCoreConfig;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
await prompts.log.warn(`Warning: Could not read existing core config: ${error.message}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Also check cache directory for custom modules (like quick update does)
|
|
525
|
+
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
526
|
+
if (await fs.pathExists(cacheDir)) {
|
|
527
|
+
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
528
|
+
|
|
529
|
+
for (const cachedModule of cachedModules) {
|
|
530
|
+
const moduleId = cachedModule.name;
|
|
531
|
+
const cachedPath = path.join(cacheDir, moduleId);
|
|
532
|
+
|
|
533
|
+
// Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
|
534
|
+
if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Skip if we already have this module from manifest
|
|
539
|
+
if (customModulePaths.has(moduleId)) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Check if this is an external official module - skip cache for those
|
|
544
|
+
const isExternal = await this.moduleManager.isExternalModule(moduleId);
|
|
545
|
+
if (isExternal) {
|
|
546
|
+
// External modules are handled via cloneExternalModule, not from cache
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Check if this is actually a custom module (has module.yaml)
|
|
551
|
+
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
|
552
|
+
if (await fs.pathExists(moduleYamlPath)) {
|
|
553
|
+
customModulePaths.set(moduleId, cachedPath);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Update module manager with the new custom module paths from cache
|
|
558
|
+
this.moduleManager.setCustomModulePaths(customModulePaths);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// If there are custom files, back them up temporarily
|
|
562
|
+
if (customFiles.length > 0) {
|
|
563
|
+
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
|
|
564
|
+
await fs.ensureDir(tempBackupDir);
|
|
565
|
+
|
|
566
|
+
spinner.start(`Backing up ${customFiles.length} custom files...`);
|
|
567
|
+
for (const customFile of customFiles) {
|
|
568
|
+
const relativePath = path.relative(bmadDir, customFile);
|
|
569
|
+
const backupPath = path.join(tempBackupDir, relativePath);
|
|
570
|
+
await fs.ensureDir(path.dirname(backupPath));
|
|
571
|
+
await fs.copy(customFile, backupPath);
|
|
572
|
+
}
|
|
573
|
+
spinner.stop(`Backed up ${customFiles.length} custom files`);
|
|
574
|
+
|
|
575
|
+
config._tempBackupDir = tempBackupDir;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// For modified files, back them up to temp directory (will be restored as .bak files after install)
|
|
579
|
+
if (modifiedFiles.length > 0) {
|
|
580
|
+
const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
|
|
581
|
+
await fs.ensureDir(tempModifiedBackupDir);
|
|
582
|
+
|
|
583
|
+
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
|
|
584
|
+
for (const modifiedFile of modifiedFiles) {
|
|
585
|
+
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
|
586
|
+
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
|
|
587
|
+
await fs.ensureDir(path.dirname(tempBackupPath));
|
|
588
|
+
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
|
|
589
|
+
}
|
|
590
|
+
spinner.stop(`Backed up ${modifiedFiles.length} modified files`);
|
|
591
|
+
|
|
592
|
+
config._tempModifiedBackupDir = tempModifiedBackupDir;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} else if (existingInstall.installed && config._quickUpdate) {
|
|
596
|
+
// Quick update mode - automatically treat as update without prompting
|
|
597
|
+
spinner.message('Preparing quick update...');
|
|
598
|
+
config._isUpdate = true;
|
|
599
|
+
config._existingInstall = existingInstall;
|
|
600
|
+
|
|
601
|
+
// Detect custom and modified files BEFORE updating
|
|
602
|
+
const existingFilesManifest = await this.readFilesManifest(bmadDir);
|
|
603
|
+
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
|
|
604
|
+
|
|
605
|
+
config._customFiles = customFiles;
|
|
606
|
+
config._modifiedFiles = modifiedFiles;
|
|
607
|
+
|
|
608
|
+
// Also check cache directory for custom modules (like quick update does)
|
|
609
|
+
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
610
|
+
if (await fs.pathExists(cacheDir)) {
|
|
611
|
+
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
612
|
+
|
|
613
|
+
for (const cachedModule of cachedModules) {
|
|
614
|
+
const moduleId = cachedModule.name;
|
|
615
|
+
const cachedPath = path.join(cacheDir, moduleId);
|
|
616
|
+
|
|
617
|
+
// Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
|
618
|
+
if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Skip if we already have this module from manifest
|
|
623
|
+
if (customModulePaths.has(moduleId)) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Check if this is an external official module - skip cache for those
|
|
628
|
+
const isExternal = await this.moduleManager.isExternalModule(moduleId);
|
|
629
|
+
if (isExternal) {
|
|
630
|
+
// External modules are handled via cloneExternalModule, not from cache
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Check if this is actually a custom module (has module.yaml)
|
|
635
|
+
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
|
636
|
+
if (await fs.pathExists(moduleYamlPath)) {
|
|
637
|
+
customModulePaths.set(moduleId, cachedPath);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Update module manager with the new custom module paths from cache
|
|
642
|
+
this.moduleManager.setCustomModulePaths(customModulePaths);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Back up custom files
|
|
646
|
+
if (customFiles.length > 0) {
|
|
647
|
+
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
|
|
648
|
+
await fs.ensureDir(tempBackupDir);
|
|
649
|
+
|
|
650
|
+
spinner.start(`Backing up ${customFiles.length} custom files...`);
|
|
651
|
+
for (const customFile of customFiles) {
|
|
652
|
+
const relativePath = path.relative(bmadDir, customFile);
|
|
653
|
+
const backupPath = path.join(tempBackupDir, relativePath);
|
|
654
|
+
await fs.ensureDir(path.dirname(backupPath));
|
|
655
|
+
await fs.copy(customFile, backupPath);
|
|
656
|
+
}
|
|
657
|
+
spinner.stop(`Backed up ${customFiles.length} custom files`);
|
|
658
|
+
config._tempBackupDir = tempBackupDir;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Back up modified files
|
|
662
|
+
if (modifiedFiles.length > 0) {
|
|
663
|
+
const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
|
|
664
|
+
await fs.ensureDir(tempModifiedBackupDir);
|
|
665
|
+
|
|
666
|
+
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
|
|
667
|
+
for (const modifiedFile of modifiedFiles) {
|
|
668
|
+
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
|
669
|
+
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
|
|
670
|
+
await fs.ensureDir(path.dirname(tempBackupPath));
|
|
671
|
+
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
|
|
672
|
+
}
|
|
673
|
+
spinner.stop(`Backed up ${modifiedFiles.length} modified files`);
|
|
674
|
+
config._tempModifiedBackupDir = tempModifiedBackupDir;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Now collect tool configurations after we know if it's a reinstall
|
|
679
|
+
// Skip for quick update since we already have the IDE list
|
|
680
|
+
spinner.stop('Pre-checks complete');
|
|
681
|
+
let toolSelection;
|
|
682
|
+
if (config._quickUpdate) {
|
|
683
|
+
// Quick update already has IDEs configured, use saved configurations
|
|
684
|
+
const preConfiguredIdes = {};
|
|
685
|
+
const savedIdeConfigs = config._savedIdeConfigs || {};
|
|
686
|
+
|
|
687
|
+
for (const ide of config.ides || []) {
|
|
688
|
+
// Use saved config if available, otherwise mark as already configured (legacy)
|
|
689
|
+
if (savedIdeConfigs[ide]) {
|
|
690
|
+
preConfiguredIdes[ide] = savedIdeConfigs[ide];
|
|
691
|
+
} else {
|
|
692
|
+
preConfiguredIdes[ide] = { _alreadyConfigured: true };
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
toolSelection = {
|
|
696
|
+
ides: config.ides || [],
|
|
697
|
+
skipIde: !config.ides || config.ides.length === 0,
|
|
698
|
+
configurations: preConfiguredIdes,
|
|
699
|
+
};
|
|
700
|
+
} else {
|
|
701
|
+
// Pass pre-selected IDEs from early prompt (if available)
|
|
702
|
+
// This allows IDE selection to happen before file copying, improving UX
|
|
703
|
+
// Use config.ides if it's an array (even if empty), null means prompt
|
|
704
|
+
const preSelectedIdes = Array.isArray(config.ides) ? config.ides : null;
|
|
705
|
+
toolSelection = await this.collectToolConfigurations(
|
|
706
|
+
path.resolve(config.directory),
|
|
707
|
+
config.modules,
|
|
708
|
+
config._isFullReinstall || false,
|
|
709
|
+
config._previouslyConfiguredIdes || [],
|
|
710
|
+
preSelectedIdes,
|
|
711
|
+
config.skipPrompts || false,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Merge tool selection into config (for both quick update and regular flow)
|
|
716
|
+
config.ides = toolSelection.ides;
|
|
717
|
+
config.skipIde = toolSelection.skipIde;
|
|
718
|
+
const ideConfigurations = toolSelection.configurations;
|
|
719
|
+
|
|
720
|
+
// Detect IDEs that were previously installed but are NOT in the new selection (to be removed)
|
|
721
|
+
if (config._isUpdate && config._existingInstall) {
|
|
722
|
+
const previouslyInstalledIdes = new Set(config._existingInstall.ides || []);
|
|
723
|
+
const newlySelectedIdes = new Set(config.ides || []);
|
|
724
|
+
|
|
725
|
+
const idesToRemove = [...previouslyInstalledIdes].filter((ide) => !newlySelectedIdes.has(ide));
|
|
726
|
+
|
|
727
|
+
if (idesToRemove.length > 0) {
|
|
728
|
+
if (config.skipPrompts) {
|
|
729
|
+
// Non-interactive mode: silently preserve existing IDE configs
|
|
730
|
+
if (!config.ides) config.ides = [];
|
|
731
|
+
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
|
732
|
+
for (const ide of idesToRemove) {
|
|
733
|
+
config.ides.push(ide);
|
|
734
|
+
if (savedIdeConfigs[ide] && !ideConfigurations[ide]) {
|
|
735
|
+
ideConfigurations[ide] = savedIdeConfigs[ide];
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
} else {
|
|
739
|
+
if (spinner.isSpinning) {
|
|
740
|
+
spinner.stop('IDE changes reviewed');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
await prompts.log.warn('IDEs to be removed:');
|
|
744
|
+
for (const ide of idesToRemove) {
|
|
745
|
+
await prompts.log.error(` - ${ide}`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const confirmRemoval = await prompts.confirm({
|
|
749
|
+
message: `Remove BMAD configuration for ${idesToRemove.length} IDE(s)?`,
|
|
750
|
+
default: false,
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
if (confirmRemoval) {
|
|
754
|
+
await this.ideManager.ensureInitialized();
|
|
755
|
+
for (const ide of idesToRemove) {
|
|
756
|
+
try {
|
|
757
|
+
const handler = this.ideManager.handlers.get(ide);
|
|
758
|
+
if (handler) {
|
|
759
|
+
await handler.cleanup(projectDir);
|
|
760
|
+
}
|
|
761
|
+
await this.ideConfigManager.deleteIdeConfig(bmadDir, ide);
|
|
762
|
+
await prompts.log.message(` Removed: ${ide}`);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
await prompts.log.warn(` Warning: Failed to remove ${ide}: ${error.message}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
await prompts.log.success(` Removed ${idesToRemove.length} IDE(s)`);
|
|
768
|
+
} else {
|
|
769
|
+
await prompts.log.message(' IDE removal cancelled');
|
|
770
|
+
// Add IDEs back to selection and restore their saved configurations
|
|
771
|
+
if (!config.ides) config.ides = [];
|
|
772
|
+
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
|
773
|
+
for (const ide of idesToRemove) {
|
|
774
|
+
config.ides.push(ide);
|
|
775
|
+
if (savedIdeConfigs[ide] && !ideConfigurations[ide]) {
|
|
776
|
+
ideConfigurations[ide] = savedIdeConfigs[ide];
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
spinner.start('Preparing installation...');
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Results collector for consolidated summary
|
|
787
|
+
const results = [];
|
|
788
|
+
const addResult = (step, status, detail = '') => results.push({ step, status, detail });
|
|
789
|
+
|
|
790
|
+
if (spinner.isSpinning) {
|
|
791
|
+
spinner.message('Preparing installation...');
|
|
792
|
+
} else {
|
|
793
|
+
spinner.start('Preparing installation...');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Create bmad directory structure
|
|
797
|
+
spinner.message('Creating directory structure...');
|
|
798
|
+
await this.createDirectoryStructure(bmadDir);
|
|
799
|
+
|
|
800
|
+
// Cache custom modules if any
|
|
801
|
+
if (customModulePaths && customModulePaths.size > 0) {
|
|
802
|
+
spinner.message('Caching custom modules...');
|
|
803
|
+
const { CustomModuleCache } = require('./custom-module-cache');
|
|
804
|
+
const customCache = new CustomModuleCache(bmadDir);
|
|
805
|
+
|
|
806
|
+
for (const [moduleId, sourcePath] of customModulePaths) {
|
|
807
|
+
const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
|
|
808
|
+
sourcePath: sourcePath, // Store original path for updates
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// Update the customModulePaths to use the cached location
|
|
812
|
+
customModulePaths.set(moduleId, cachedInfo.cachePath);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Update module manager with the cached paths
|
|
816
|
+
this.moduleManager.setCustomModulePaths(customModulePaths);
|
|
817
|
+
addResult('Custom modules cached', 'ok');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const projectRoot = getProjectRoot();
|
|
821
|
+
|
|
822
|
+
// Custom content is already handled in UI before module selection
|
|
823
|
+
const finalCustomContent = config.customContent;
|
|
824
|
+
|
|
825
|
+
// Prepare modules list including cached custom modules
|
|
826
|
+
let allModules = [...(config.modules || [])];
|
|
827
|
+
|
|
828
|
+
// During quick update, we might have custom module sources from the manifest
|
|
829
|
+
if (config._customModuleSources) {
|
|
830
|
+
// Add custom modules from stored sources
|
|
831
|
+
for (const [moduleId, customInfo] of config._customModuleSources) {
|
|
832
|
+
if (!allModules.includes(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
|
|
833
|
+
allModules.push(moduleId);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Add cached custom modules
|
|
839
|
+
if (finalCustomContent && finalCustomContent.cachedModules) {
|
|
840
|
+
for (const cachedModule of finalCustomContent.cachedModules) {
|
|
841
|
+
if (!allModules.includes(cachedModule.id)) {
|
|
842
|
+
allModules.push(cachedModule.id);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Regular custom content from user input (non-cached)
|
|
848
|
+
if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
|
|
849
|
+
// Add custom modules to the installation list
|
|
850
|
+
const customHandler = new CustomHandler();
|
|
851
|
+
for (const customFile of finalCustomContent.selectedFiles) {
|
|
852
|
+
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
|
|
853
|
+
if (customInfo && customInfo.id) {
|
|
854
|
+
allModules.push(customInfo.id);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Don't include core again if already installed
|
|
860
|
+
if (config.installCore) {
|
|
861
|
+
allModules = allModules.filter((m) => m !== 'core');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// For dependency resolution, we only need regular modules (not custom modules)
|
|
865
|
+
// Custom modules are already installed in _bmad and don't need dependency resolution from source
|
|
866
|
+
const regularModulesForResolution = allModules.filter((module) => {
|
|
867
|
+
// Check if this is a custom module
|
|
868
|
+
const isCustom =
|
|
869
|
+
customModulePaths.has(module) ||
|
|
870
|
+
(finalCustomContent && finalCustomContent.cachedModules && finalCustomContent.cachedModules.some((cm) => cm.id === module)) ||
|
|
871
|
+
(finalCustomContent &&
|
|
872
|
+
finalCustomContent.selected &&
|
|
873
|
+
finalCustomContent.selectedFiles &&
|
|
874
|
+
finalCustomContent.selectedFiles.some((f) => f.includes(module)));
|
|
875
|
+
return !isCustom;
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// Stop spinner before tasks() takes over progress display
|
|
879
|
+
spinner.stop('Preparation complete');
|
|
880
|
+
|
|
881
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
882
|
+
// FIRST TASKS BLOCK: Core installation through manifests (non-interactive)
|
|
883
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
884
|
+
const isQuickUpdate = config._quickUpdate || false;
|
|
885
|
+
|
|
886
|
+
// Shared resolution result across task callbacks (closure-scoped, not on `this`)
|
|
887
|
+
let taskResolution;
|
|
888
|
+
|
|
889
|
+
// Collect directory creation results for output after tasks() completes
|
|
890
|
+
const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
|
891
|
+
|
|
892
|
+
// Build task list conditionally
|
|
893
|
+
const installTasks = [];
|
|
894
|
+
|
|
895
|
+
// Core installation task
|
|
896
|
+
if (config.installCore) {
|
|
897
|
+
installTasks.push({
|
|
898
|
+
title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core',
|
|
899
|
+
task: async (message) => {
|
|
900
|
+
await this.installCoreWithDependencies(bmadDir, { core: {} });
|
|
901
|
+
addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed');
|
|
902
|
+
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
|
|
903
|
+
return isQuickUpdate ? 'Core updated' : 'Core installed';
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Dependency resolution task
|
|
909
|
+
installTasks.push({
|
|
910
|
+
title: 'Resolving dependencies',
|
|
911
|
+
task: async (message) => {
|
|
912
|
+
// Create a temporary module manager that knows about custom content locations
|
|
913
|
+
const tempModuleManager = new ModuleManager({
|
|
914
|
+
bmadDir: bmadDir,
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
taskResolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
|
|
918
|
+
verbose: config.verbose,
|
|
919
|
+
moduleManager: tempModuleManager,
|
|
920
|
+
});
|
|
921
|
+
return 'Dependencies resolved';
|
|
922
|
+
},
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
// Module installation task
|
|
926
|
+
if (allModules && allModules.length > 0) {
|
|
927
|
+
installTasks.push({
|
|
928
|
+
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
|
|
929
|
+
task: async (message) => {
|
|
930
|
+
const resolution = taskResolution;
|
|
931
|
+
const installedModuleNames = new Set();
|
|
932
|
+
|
|
933
|
+
for (const moduleName of allModules) {
|
|
934
|
+
if (installedModuleNames.has(moduleName)) continue;
|
|
935
|
+
installedModuleNames.add(moduleName);
|
|
936
|
+
|
|
937
|
+
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
|
938
|
+
|
|
939
|
+
// Check if this is a custom module
|
|
940
|
+
let isCustomModule = false;
|
|
941
|
+
let customInfo = null;
|
|
942
|
+
|
|
943
|
+
// First check if we have a cached version
|
|
944
|
+
if (finalCustomContent && finalCustomContent.cachedModules) {
|
|
945
|
+
const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
|
|
946
|
+
if (cachedModule) {
|
|
947
|
+
isCustomModule = true;
|
|
948
|
+
customInfo = { id: moduleName, path: cachedModule.cachePath, config: {} };
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Then check custom module sources from manifest (for quick update)
|
|
953
|
+
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
|
|
954
|
+
customInfo = config._customModuleSources.get(moduleName);
|
|
955
|
+
isCustomModule = true;
|
|
956
|
+
if (customInfo.sourcePath && !customInfo.path) {
|
|
957
|
+
customInfo.path = path.isAbsolute(customInfo.sourcePath)
|
|
958
|
+
? customInfo.sourcePath
|
|
959
|
+
: path.join(bmadDir, customInfo.sourcePath);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Finally check regular custom content
|
|
964
|
+
if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
|
|
965
|
+
const customHandler = new CustomHandler();
|
|
966
|
+
for (const customFile of finalCustomContent.selectedFiles) {
|
|
967
|
+
const info = await customHandler.getCustomInfo(customFile, projectDir);
|
|
968
|
+
if (info && info.id === moduleName) {
|
|
969
|
+
isCustomModule = true;
|
|
970
|
+
customInfo = info;
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (isCustomModule && customInfo) {
|
|
977
|
+
if (!customModulePaths.has(moduleName) && customInfo.path) {
|
|
978
|
+
customModulePaths.set(moduleName, customInfo.path);
|
|
979
|
+
this.moduleManager.setCustomModulePaths(customModulePaths);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const collectedModuleConfig = moduleConfigs[moduleName] || {};
|
|
983
|
+
await this.moduleManager.install(
|
|
984
|
+
moduleName,
|
|
985
|
+
bmadDir,
|
|
986
|
+
(filePath) => {
|
|
987
|
+
this.installedFiles.add(filePath);
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
isCustom: true,
|
|
991
|
+
moduleConfig: collectedModuleConfig,
|
|
992
|
+
isQuickUpdate: isQuickUpdate,
|
|
993
|
+
installer: this,
|
|
994
|
+
silent: true,
|
|
995
|
+
},
|
|
996
|
+
);
|
|
997
|
+
await this.generateModuleConfigs(bmadDir, {
|
|
998
|
+
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
|
999
|
+
});
|
|
1000
|
+
} else {
|
|
1001
|
+
if (!resolution || !resolution.byModule) {
|
|
1002
|
+
addResult(`Module: ${moduleName}`, 'warn', 'skipped (no resolution data)');
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (moduleName === 'core') {
|
|
1006
|
+
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
|
|
1007
|
+
} else {
|
|
1008
|
+
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Install partial modules (only dependencies)
|
|
1016
|
+
if (!resolution || !resolution.byModule) {
|
|
1017
|
+
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
|
|
1018
|
+
}
|
|
1019
|
+
for (const [module, files] of Object.entries(resolution.byModule)) {
|
|
1020
|
+
if (!allModules.includes(module) && module !== 'core') {
|
|
1021
|
+
const totalFiles =
|
|
1022
|
+
files.agents.length +
|
|
1023
|
+
files.tasks.length +
|
|
1024
|
+
files.tools.length +
|
|
1025
|
+
files.templates.length +
|
|
1026
|
+
files.data.length +
|
|
1027
|
+
files.other.length;
|
|
1028
|
+
if (totalFiles > 0) {
|
|
1029
|
+
message(`Installing ${module} dependencies...`);
|
|
1030
|
+
await this.installPartialModule(module, bmadDir, files);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Module directory creation task
|
|
1041
|
+
installTasks.push({
|
|
1042
|
+
title: 'Creating module directories',
|
|
1043
|
+
task: async (message) => {
|
|
1044
|
+
const resolution = taskResolution;
|
|
1045
|
+
if (!resolution || !resolution.byModule) {
|
|
1046
|
+
addResult('Module directories', 'warn', 'no resolution data');
|
|
1047
|
+
return 'Module directories skipped (no resolution data)';
|
|
1048
|
+
}
|
|
1049
|
+
const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
|
|
1050
|
+
const moduleLogger = {
|
|
1051
|
+
log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined),
|
|
1052
|
+
error: async (msg) => await prompts.log.error(msg),
|
|
1053
|
+
warn: async (msg) => await prompts.log.warn(msg),
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
// Core module directories
|
|
1057
|
+
if (config.installCore || resolution.byModule.core) {
|
|
1058
|
+
const result = await this.moduleManager.createModuleDirectories('core', bmadDir, {
|
|
1059
|
+
installedIDEs: config.ides || [],
|
|
1060
|
+
moduleConfig: moduleConfigs.core || {},
|
|
1061
|
+
existingModuleConfig: this.configCollector.existingConfig?.core || {},
|
|
1062
|
+
coreConfig: moduleConfigs.core || {},
|
|
1063
|
+
logger: moduleLogger,
|
|
1064
|
+
silent: true,
|
|
1065
|
+
});
|
|
1066
|
+
if (result) {
|
|
1067
|
+
dirResults.createdDirs.push(...result.createdDirs);
|
|
1068
|
+
dirResults.movedDirs.push(...(result.movedDirs || []));
|
|
1069
|
+
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// User-selected module directories
|
|
1074
|
+
if (config.modules && config.modules.length > 0) {
|
|
1075
|
+
for (const moduleName of config.modules) {
|
|
1076
|
+
message(`Setting up ${moduleName}...`);
|
|
1077
|
+
const result = await this.moduleManager.createModuleDirectories(moduleName, bmadDir, {
|
|
1078
|
+
installedIDEs: config.ides || [],
|
|
1079
|
+
moduleConfig: moduleConfigs[moduleName] || {},
|
|
1080
|
+
existingModuleConfig: this.configCollector.existingConfig?.[moduleName] || {},
|
|
1081
|
+
coreConfig: moduleConfigs.core || {},
|
|
1082
|
+
logger: moduleLogger,
|
|
1083
|
+
silent: true,
|
|
1084
|
+
});
|
|
1085
|
+
if (result) {
|
|
1086
|
+
dirResults.createdDirs.push(...result.createdDirs);
|
|
1087
|
+
dirResults.movedDirs.push(...(result.movedDirs || []));
|
|
1088
|
+
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
addResult('Module directories', 'ok');
|
|
1094
|
+
return 'Module directories created';
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// Configuration generation task (stored as named reference for deferred execution)
|
|
1099
|
+
const configTask = {
|
|
1100
|
+
title: 'Generating configurations',
|
|
1101
|
+
task: async (message) => {
|
|
1102
|
+
// Generate clean config.yaml files for each installed module
|
|
1103
|
+
await this.generateModuleConfigs(bmadDir, moduleConfigs);
|
|
1104
|
+
addResult('Configurations', 'ok', 'generated');
|
|
1105
|
+
|
|
1106
|
+
// Pre-register manifest files
|
|
1107
|
+
const cfgDir = path.join(bmadDir, '_config');
|
|
1108
|
+
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
|
|
1109
|
+
this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
|
|
1110
|
+
this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
|
|
1111
|
+
this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
|
|
1112
|
+
|
|
1113
|
+
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes
|
|
1114
|
+
// This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
|
|
1115
|
+
message('Generating manifests...');
|
|
1116
|
+
const manifestGen = new ManifestGenerator();
|
|
1117
|
+
|
|
1118
|
+
const allModulesForManifest = config._quickUpdate
|
|
1119
|
+
? config._existingModules || allModules || []
|
|
1120
|
+
: config._preserveModules
|
|
1121
|
+
? [...allModules, ...config._preserveModules]
|
|
1122
|
+
: allModules || [];
|
|
1123
|
+
|
|
1124
|
+
let modulesForCsvPreserve;
|
|
1125
|
+
if (config._quickUpdate) {
|
|
1126
|
+
modulesForCsvPreserve = config._existingModules || allModules || [];
|
|
1127
|
+
} else {
|
|
1128
|
+
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
|
|
1132
|
+
ides: config.ides || [],
|
|
1133
|
+
preservedModules: modulesForCsvPreserve,
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
addResult(
|
|
1137
|
+
'Manifests',
|
|
1138
|
+
'ok',
|
|
1139
|
+
`${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`,
|
|
1140
|
+
);
|
|
1141
|
+
|
|
1142
|
+
// Merge help catalogs
|
|
1143
|
+
message('Generating help catalog...');
|
|
1144
|
+
await this.mergeModuleHelpCatalogs(bmadDir);
|
|
1145
|
+
addResult('Help catalog', 'ok');
|
|
1146
|
+
|
|
1147
|
+
return 'Configurations generated';
|
|
1148
|
+
},
|
|
1149
|
+
};
|
|
1150
|
+
installTasks.push(configTask);
|
|
1151
|
+
|
|
1152
|
+
// Run all tasks except config (which runs after directory output)
|
|
1153
|
+
const mainTasks = installTasks.filter((t) => t !== configTask);
|
|
1154
|
+
await prompts.tasks(mainTasks);
|
|
1155
|
+
|
|
1156
|
+
// Render directory creation output right after directory task
|
|
1157
|
+
const color = await prompts.getColor();
|
|
1158
|
+
if (dirResults.movedDirs.length > 0) {
|
|
1159
|
+
const lines = dirResults.movedDirs.map((d) => ` ${d}`).join('\n');
|
|
1160
|
+
await prompts.log.message(color.cyan(`Moved directories:\n${lines}`));
|
|
1161
|
+
}
|
|
1162
|
+
if (dirResults.createdDirs.length > 0) {
|
|
1163
|
+
const lines = dirResults.createdDirs.map((d) => ` ${d}`).join('\n');
|
|
1164
|
+
await prompts.log.message(color.yellow(`Created directories:\n${lines}`));
|
|
1165
|
+
}
|
|
1166
|
+
if (dirResults.createdWdsFolders.length > 0) {
|
|
1167
|
+
const lines = dirResults.createdWdsFolders.map((f) => color.dim(` \u2713 ${f}/`)).join('\n');
|
|
1168
|
+
await prompts.log.message(color.cyan(`Created WDS folder structure:\n${lines}`));
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Now run configuration generation
|
|
1172
|
+
await prompts.tasks([configTask]);
|
|
1173
|
+
|
|
1174
|
+
// Resolution is now available via closure-scoped taskResolution
|
|
1175
|
+
const resolution = taskResolution;
|
|
1176
|
+
|
|
1177
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1178
|
+
// IDE SETUP: Keep as spinner since it may prompt for user input
|
|
1179
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1180
|
+
if (!config.skipIde && config.ides && config.ides.length > 0) {
|
|
1181
|
+
await this.ideManager.ensureInitialized();
|
|
1182
|
+
const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
|
|
1183
|
+
|
|
1184
|
+
if (validIdes.length === 0) {
|
|
1185
|
+
addResult('IDE configuration', 'warn', 'no valid IDEs selected');
|
|
1186
|
+
} else {
|
|
1187
|
+
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
|
|
1188
|
+
const ideSpinner = await prompts.spinner();
|
|
1189
|
+
ideSpinner.start('Configuring tools...');
|
|
1190
|
+
|
|
1191
|
+
try {
|
|
1192
|
+
for (const ide of validIdes) {
|
|
1193
|
+
if (!needsPrompting || ideConfigurations[ide]) {
|
|
1194
|
+
ideSpinner.message(`Configuring ${ide}...`);
|
|
1195
|
+
} else {
|
|
1196
|
+
if (ideSpinner.isSpinning) {
|
|
1197
|
+
ideSpinner.stop('Ready for IDE configuration');
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Suppress stray console output for pre-configured IDEs (no user interaction)
|
|
1202
|
+
const ideHasConfig = Boolean(ideConfigurations[ide]);
|
|
1203
|
+
const originalLog = console.log;
|
|
1204
|
+
if (!config.verbose && ideHasConfig) {
|
|
1205
|
+
console.log = () => {};
|
|
1206
|
+
}
|
|
1207
|
+
try {
|
|
1208
|
+
const setupResult = await this.ideManager.setup(ide, projectDir, bmadDir, {
|
|
1209
|
+
selectedModules: allModules || [],
|
|
1210
|
+
preCollectedConfig: ideConfigurations[ide] || null,
|
|
1211
|
+
verbose: config.verbose,
|
|
1212
|
+
silent: ideHasConfig,
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
|
|
1216
|
+
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (setupResult.success) {
|
|
1220
|
+
addResult(ide, 'ok', setupResult.detail || '');
|
|
1221
|
+
} else {
|
|
1222
|
+
addResult(ide, 'error', setupResult.error || 'failed');
|
|
1223
|
+
}
|
|
1224
|
+
} finally {
|
|
1225
|
+
console.log = originalLog;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (needsPrompting && !ideSpinner.isSpinning) {
|
|
1229
|
+
ideSpinner.start('Configuring tools...');
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
} finally {
|
|
1233
|
+
if (ideSpinner.isSpinning) {
|
|
1234
|
+
ideSpinner.stop('Tool configuration complete');
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1241
|
+
// SECOND TASKS BLOCK: Post-IDE operations (non-interactive)
|
|
1242
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1243
|
+
const postIdeTasks = [];
|
|
1244
|
+
|
|
1245
|
+
// File restoration task (only for updates)
|
|
1246
|
+
if (
|
|
1247
|
+
config._isUpdate &&
|
|
1248
|
+
((config._customFiles && config._customFiles.length > 0) || (config._modifiedFiles && config._modifiedFiles.length > 0))
|
|
1249
|
+
) {
|
|
1250
|
+
postIdeTasks.push({
|
|
1251
|
+
title: 'Finalizing installation',
|
|
1252
|
+
task: async (message) => {
|
|
1253
|
+
let customFiles = [];
|
|
1254
|
+
let modifiedFiles = [];
|
|
1255
|
+
|
|
1256
|
+
if (config._customFiles && config._customFiles.length > 0) {
|
|
1257
|
+
message(`Restoring ${config._customFiles.length} custom files...`);
|
|
1258
|
+
|
|
1259
|
+
for (const originalPath of config._customFiles) {
|
|
1260
|
+
const relativePath = path.relative(bmadDir, originalPath);
|
|
1261
|
+
const backupPath = path.join(config._tempBackupDir, relativePath);
|
|
1262
|
+
|
|
1263
|
+
if (await fs.pathExists(backupPath)) {
|
|
1264
|
+
await fs.ensureDir(path.dirname(originalPath));
|
|
1265
|
+
await fs.copy(backupPath, originalPath, { overwrite: true });
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
|
|
1270
|
+
await fs.remove(config._tempBackupDir);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
customFiles = config._customFiles;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (config._modifiedFiles && config._modifiedFiles.length > 0) {
|
|
1277
|
+
modifiedFiles = config._modifiedFiles;
|
|
1278
|
+
|
|
1279
|
+
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
|
|
1280
|
+
message(`Restoring ${modifiedFiles.length} modified files as .bak...`);
|
|
1281
|
+
|
|
1282
|
+
for (const modifiedFile of modifiedFiles) {
|
|
1283
|
+
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
|
1284
|
+
const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath);
|
|
1285
|
+
const bakPath = modifiedFile.path + '.bak';
|
|
1286
|
+
|
|
1287
|
+
if (await fs.pathExists(tempBackupPath)) {
|
|
1288
|
+
await fs.ensureDir(path.dirname(bakPath));
|
|
1289
|
+
await fs.copy(tempBackupPath, bakPath, { overwrite: true });
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
await fs.remove(config._tempModifiedBackupDir);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Store for summary access
|
|
1298
|
+
config._restoredCustomFiles = customFiles;
|
|
1299
|
+
config._restoredModifiedFiles = modifiedFiles;
|
|
1300
|
+
|
|
1301
|
+
return 'Installation finalized';
|
|
1302
|
+
},
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
await prompts.tasks(postIdeTasks);
|
|
1307
|
+
|
|
1308
|
+
// Retrieve restored file info for summary
|
|
1309
|
+
const customFiles = config._restoredCustomFiles || [];
|
|
1310
|
+
const modifiedFiles = config._restoredModifiedFiles || [];
|
|
1311
|
+
|
|
1312
|
+
// Render consolidated summary
|
|
1313
|
+
await this.renderInstallSummary(results, {
|
|
1314
|
+
bmadDir,
|
|
1315
|
+
modules: config.modules,
|
|
1316
|
+
ides: config.ides,
|
|
1317
|
+
customFiles: customFiles.length > 0 ? customFiles : undefined,
|
|
1318
|
+
modifiedFiles: modifiedFiles.length > 0 ? modifiedFiles : undefined,
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
return {
|
|
1322
|
+
success: true,
|
|
1323
|
+
path: bmadDir,
|
|
1324
|
+
modules: config.modules,
|
|
1325
|
+
ides: config.ides,
|
|
1326
|
+
projectDir: projectDir,
|
|
1327
|
+
};
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
try {
|
|
1330
|
+
if (spinner.isSpinning) {
|
|
1331
|
+
spinner.error('Installation failed');
|
|
1332
|
+
} else {
|
|
1333
|
+
await prompts.log.error('Installation failed');
|
|
1334
|
+
}
|
|
1335
|
+
} catch {
|
|
1336
|
+
// Ensure the original error is never swallowed by a logging failure
|
|
1337
|
+
}
|
|
1338
|
+
throw error;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Render a consolidated install summary using prompts.note()
|
|
1344
|
+
* @param {Array} results - Array of {step, status: 'ok'|'error'|'warn', detail}
|
|
1345
|
+
* @param {Object} context - {bmadDir, modules, ides, customFiles, modifiedFiles}
|
|
1346
|
+
*/
|
|
1347
|
+
async renderInstallSummary(results, context = {}) {
|
|
1348
|
+
const color = await prompts.getColor();
|
|
1349
|
+
|
|
1350
|
+
// Build step lines with status indicators
|
|
1351
|
+
const lines = [];
|
|
1352
|
+
for (const r of results) {
|
|
1353
|
+
let icon;
|
|
1354
|
+
if (r.status === 'ok') {
|
|
1355
|
+
icon = color.green('\u2713');
|
|
1356
|
+
} else if (r.status === 'warn') {
|
|
1357
|
+
icon = color.yellow('!');
|
|
1358
|
+
} else {
|
|
1359
|
+
icon = color.red('\u2717');
|
|
1360
|
+
}
|
|
1361
|
+
const detail = r.detail ? color.dim(` (${r.detail})`) : '';
|
|
1362
|
+
lines.push(` ${icon} ${r.step}${detail}`);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Context and warnings
|
|
1366
|
+
lines.push('');
|
|
1367
|
+
if (context.bmadDir) {
|
|
1368
|
+
lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
|
|
1369
|
+
}
|
|
1370
|
+
if (context.customFiles && context.customFiles.length > 0) {
|
|
1371
|
+
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
|
|
1372
|
+
}
|
|
1373
|
+
if (context.modifiedFiles && context.modifiedFiles.length > 0) {
|
|
1374
|
+
lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Next steps
|
|
1378
|
+
lines.push(
|
|
1379
|
+
'',
|
|
1380
|
+
' Next steps:',
|
|
1381
|
+
` Read our new Docs Site: ${color.dim('https://docs.bmad-method.org/')}`,
|
|
1382
|
+
` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
|
|
1383
|
+
` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`,
|
|
1384
|
+
` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`,
|
|
1385
|
+
` Run ${color.cyan('/bmad-help')} with your IDE Agent and ask it how to get started`,
|
|
1386
|
+
);
|
|
1387
|
+
|
|
1388
|
+
await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Update existing installation
|
|
1393
|
+
*/
|
|
1394
|
+
async update(config) {
|
|
1395
|
+
const spinner = await prompts.spinner();
|
|
1396
|
+
spinner.start('Checking installation...');
|
|
1397
|
+
|
|
1398
|
+
try {
|
|
1399
|
+
const projectDir = path.resolve(config.directory);
|
|
1400
|
+
const { bmadDir } = await this.findBmadDir(projectDir);
|
|
1401
|
+
const existingInstall = await this.detector.detect(bmadDir);
|
|
1402
|
+
|
|
1403
|
+
if (!existingInstall.installed) {
|
|
1404
|
+
spinner.stop('No BMAD installation found');
|
|
1405
|
+
throw new Error(`No BMAD installation found at ${bmadDir}`);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
spinner.message('Analyzing update requirements...');
|
|
1409
|
+
|
|
1410
|
+
// Compare versions and determine what needs updating
|
|
1411
|
+
const currentVersion = existingInstall.version;
|
|
1412
|
+
const newVersion = require(path.join(getProjectRoot(), 'package.json')).version;
|
|
1413
|
+
|
|
1414
|
+
// Check for custom modules with missing sources before update
|
|
1415
|
+
const customModuleSources = new Map();
|
|
1416
|
+
|
|
1417
|
+
// Check manifest for backward compatibility
|
|
1418
|
+
if (existingInstall.customModules) {
|
|
1419
|
+
for (const customModule of existingInstall.customModules) {
|
|
1420
|
+
customModuleSources.set(customModule.id, customModule);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Also check cache directory
|
|
1425
|
+
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
1426
|
+
if (await fs.pathExists(cacheDir)) {
|
|
1427
|
+
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
1428
|
+
|
|
1429
|
+
for (const cachedModule of cachedModules) {
|
|
1430
|
+
if (cachedModule.isDirectory()) {
|
|
1431
|
+
const moduleId = cachedModule.name;
|
|
1432
|
+
|
|
1433
|
+
// Skip if we already have this module
|
|
1434
|
+
if (customModuleSources.has(moduleId)) {
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// Check if this is an external official module - skip cache for those
|
|
1439
|
+
const isExternal = await this.moduleManager.isExternalModule(moduleId);
|
|
1440
|
+
if (isExternal) {
|
|
1441
|
+
// External modules are handled via cloneExternalModule, not from cache
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const cachedPath = path.join(cacheDir, moduleId);
|
|
1446
|
+
|
|
1447
|
+
// Check if this is actually a custom module (has module.yaml)
|
|
1448
|
+
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
|
1449
|
+
if (await fs.pathExists(moduleYamlPath)) {
|
|
1450
|
+
customModuleSources.set(moduleId, {
|
|
1451
|
+
id: moduleId,
|
|
1452
|
+
name: moduleId,
|
|
1453
|
+
sourcePath: path.join('_config', 'custom', moduleId), // Relative path
|
|
1454
|
+
cached: true,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
if (customModuleSources.size > 0) {
|
|
1462
|
+
spinner.stop('Update analysis complete');
|
|
1463
|
+
await prompts.log.warn('Checking custom module sources before update...');
|
|
1464
|
+
|
|
1465
|
+
const projectRoot = getProjectRoot();
|
|
1466
|
+
await this.handleMissingCustomSources(
|
|
1467
|
+
customModuleSources,
|
|
1468
|
+
bmadDir,
|
|
1469
|
+
projectRoot,
|
|
1470
|
+
'update',
|
|
1471
|
+
existingInstall.modules.map((m) => m.id),
|
|
1472
|
+
config.skipPrompts || false,
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
spinner.start('Preparing update...');
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (config.dryRun) {
|
|
1479
|
+
spinner.stop('Dry run analysis complete');
|
|
1480
|
+
let dryRunContent = `Current version: ${currentVersion}\n`;
|
|
1481
|
+
dryRunContent += `New version: ${newVersion}\n`;
|
|
1482
|
+
dryRunContent += `Core: ${existingInstall.hasCore ? 'Will be updated' : 'Not installed'}`;
|
|
1483
|
+
|
|
1484
|
+
if (existingInstall.modules.length > 0) {
|
|
1485
|
+
dryRunContent += '\n\nModules to update:';
|
|
1486
|
+
for (const mod of existingInstall.modules) {
|
|
1487
|
+
dryRunContent += `\n - ${mod.id}`;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
await prompts.note(dryRunContent, 'Update Preview (Dry Run)');
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Perform actual update
|
|
1495
|
+
if (existingInstall.hasCore) {
|
|
1496
|
+
spinner.message('Updating core...');
|
|
1497
|
+
await this.updateCore(bmadDir, config.force);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
for (const module of existingInstall.modules) {
|
|
1501
|
+
spinner.message(`Updating module: ${module.id}...`);
|
|
1502
|
+
await this.moduleManager.update(module.id, bmadDir, config.force, { installer: this });
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Update manifest
|
|
1506
|
+
spinner.message('Updating manifest...');
|
|
1507
|
+
await this.manifest.update(bmadDir, {
|
|
1508
|
+
version: newVersion,
|
|
1509
|
+
updateDate: new Date().toISOString(),
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
spinner.stop('Update complete');
|
|
1513
|
+
return { success: true };
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
spinner.error('Update failed');
|
|
1516
|
+
throw error;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* Get installation status
|
|
1522
|
+
*/
|
|
1523
|
+
async getStatus(directory) {
|
|
1524
|
+
const projectDir = path.resolve(directory);
|
|
1525
|
+
const { bmadDir } = await this.findBmadDir(projectDir);
|
|
1526
|
+
return await this.detector.detect(bmadDir);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Get available modules
|
|
1531
|
+
*/
|
|
1532
|
+
async getAvailableModules() {
|
|
1533
|
+
return await this.moduleManager.listAvailable();
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Uninstall BMAD with selective removal options
|
|
1538
|
+
* @param {string} directory - Project directory
|
|
1539
|
+
* @param {Object} options - Uninstall options
|
|
1540
|
+
* @param {boolean} [options.removeModules=true] - Remove _bmad/ directory
|
|
1541
|
+
* @param {boolean} [options.removeIdeConfigs=true] - Remove IDE configurations
|
|
1542
|
+
* @param {boolean} [options.removeOutputFolder=false] - Remove user artifacts output folder
|
|
1543
|
+
* @returns {Object} Result with success status and removed components
|
|
1544
|
+
*/
|
|
1545
|
+
async uninstall(directory, options = {}) {
|
|
1546
|
+
const projectDir = path.resolve(directory);
|
|
1547
|
+
const { bmadDir } = await this.findBmadDir(projectDir);
|
|
1548
|
+
|
|
1549
|
+
if (!(await fs.pathExists(bmadDir))) {
|
|
1550
|
+
return { success: false, reason: 'not-installed' };
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// 1. DETECT: Read state BEFORE deleting anything
|
|
1554
|
+
const existingInstall = await this.detector.detect(bmadDir);
|
|
1555
|
+
const outputFolder = await this._readOutputFolder(bmadDir);
|
|
1556
|
+
|
|
1557
|
+
const removed = { modules: false, ideConfigs: false, outputFolder: false };
|
|
1558
|
+
|
|
1559
|
+
// 2. IDE CLEANUP (before _bmad/ deletion so configs are accessible)
|
|
1560
|
+
if (options.removeIdeConfigs !== false) {
|
|
1561
|
+
await this.uninstallIdeConfigs(projectDir, existingInstall, { silent: options.silent });
|
|
1562
|
+
removed.ideConfigs = true;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// 3. OUTPUT FOLDER (only if explicitly requested)
|
|
1566
|
+
if (options.removeOutputFolder === true && outputFolder) {
|
|
1567
|
+
removed.outputFolder = await this.uninstallOutputFolder(projectDir, outputFolder);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// 4. BMAD DIRECTORY (last, after everything that needs it)
|
|
1571
|
+
if (options.removeModules !== false) {
|
|
1572
|
+
removed.modules = await this.uninstallModules(projectDir);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
return { success: true, removed, version: existingInstall.version };
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Uninstall IDE configurations only
|
|
1580
|
+
* @param {string} projectDir - Project directory
|
|
1581
|
+
* @param {Object} existingInstall - Detection result from detector.detect()
|
|
1582
|
+
* @param {Object} [options] - Options (e.g. { silent: true })
|
|
1583
|
+
* @returns {Promise<Object>} Results from IDE cleanup
|
|
1584
|
+
*/
|
|
1585
|
+
async uninstallIdeConfigs(projectDir, existingInstall, options = {}) {
|
|
1586
|
+
await this.ideManager.ensureInitialized();
|
|
1587
|
+
const cleanupOptions = { isUninstall: true, silent: options.silent };
|
|
1588
|
+
const ideList = existingInstall.ides || [];
|
|
1589
|
+
if (ideList.length > 0) {
|
|
1590
|
+
return this.ideManager.cleanupByList(projectDir, ideList, cleanupOptions);
|
|
1591
|
+
}
|
|
1592
|
+
return this.ideManager.cleanup(projectDir, cleanupOptions);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* Remove user artifacts output folder
|
|
1597
|
+
* @param {string} projectDir - Project directory
|
|
1598
|
+
* @param {string} outputFolder - Output folder name (relative)
|
|
1599
|
+
* @returns {Promise<boolean>} Whether the folder was removed
|
|
1600
|
+
*/
|
|
1601
|
+
async uninstallOutputFolder(projectDir, outputFolder) {
|
|
1602
|
+
if (!outputFolder) return false;
|
|
1603
|
+
const resolvedProject = path.resolve(projectDir);
|
|
1604
|
+
const outputPath = path.resolve(resolvedProject, outputFolder);
|
|
1605
|
+
if (!outputPath.startsWith(resolvedProject + path.sep)) {
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
if (await fs.pathExists(outputPath)) {
|
|
1609
|
+
await fs.remove(outputPath);
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* Remove the _bmad/ directory
|
|
1617
|
+
* @param {string} projectDir - Project directory
|
|
1618
|
+
* @returns {Promise<boolean>} Whether the directory was removed
|
|
1619
|
+
*/
|
|
1620
|
+
async uninstallModules(projectDir) {
|
|
1621
|
+
const { bmadDir } = await this.findBmadDir(projectDir);
|
|
1622
|
+
if (await fs.pathExists(bmadDir)) {
|
|
1623
|
+
await fs.remove(bmadDir);
|
|
1624
|
+
return true;
|
|
1625
|
+
}
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Get the configured output folder name for a project
|
|
1631
|
+
* Resolves bmadDir internally from projectDir
|
|
1632
|
+
* @param {string} projectDir - Project directory
|
|
1633
|
+
* @returns {string} Output folder name (relative, default: '_bmad-output')
|
|
1634
|
+
*/
|
|
1635
|
+
async getOutputFolder(projectDir) {
|
|
1636
|
+
const { bmadDir } = await this.findBmadDir(projectDir);
|
|
1637
|
+
return this._readOutputFolder(bmadDir);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Read the output_folder setting from module config files
|
|
1642
|
+
* Checks bmm/config.yaml first, then other module configs
|
|
1643
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
1644
|
+
* @returns {string} Output folder path or default
|
|
1645
|
+
*/
|
|
1646
|
+
async _readOutputFolder(bmadDir) {
|
|
1647
|
+
const yaml = require('yaml');
|
|
1648
|
+
|
|
1649
|
+
// Check bmm/config.yaml first (most common)
|
|
1650
|
+
const bmmConfigPath = path.join(bmadDir, 'bmm', 'config.yaml');
|
|
1651
|
+
if (await fs.pathExists(bmmConfigPath)) {
|
|
1652
|
+
try {
|
|
1653
|
+
const content = await fs.readFile(bmmConfigPath, 'utf8');
|
|
1654
|
+
const config = yaml.parse(content);
|
|
1655
|
+
if (config && config.output_folder) {
|
|
1656
|
+
// Strip {project-root}/ prefix if present
|
|
1657
|
+
return config.output_folder.replace(/^\{project-root\}[/\\]/, '');
|
|
1658
|
+
}
|
|
1659
|
+
} catch {
|
|
1660
|
+
// Fall through to other modules
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Scan other module config.yaml files
|
|
1665
|
+
try {
|
|
1666
|
+
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
1667
|
+
for (const entry of entries) {
|
|
1668
|
+
if (!entry.isDirectory() || entry.name === 'bmm' || entry.name.startsWith('_')) continue;
|
|
1669
|
+
const configPath = path.join(bmadDir, entry.name, 'config.yaml');
|
|
1670
|
+
if (await fs.pathExists(configPath)) {
|
|
1671
|
+
try {
|
|
1672
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
1673
|
+
const config = yaml.parse(content);
|
|
1674
|
+
if (config && config.output_folder) {
|
|
1675
|
+
return config.output_folder.replace(/^\{project-root\}[/\\]/, '');
|
|
1676
|
+
}
|
|
1677
|
+
} catch {
|
|
1678
|
+
// Continue scanning
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
} catch {
|
|
1683
|
+
// Directory scan failed
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// Default fallback
|
|
1687
|
+
return '_bmad-output';
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Private: Create directory structure
|
|
1692
|
+
*/
|
|
1693
|
+
/**
|
|
1694
|
+
* Merge all module-help.csv files into a single bmad-help.csv
|
|
1695
|
+
* Scans all installed modules for module-help.csv and merges them
|
|
1696
|
+
* Enriches agent info from agent-manifest.csv
|
|
1697
|
+
* Output is written to _bmad/_config/bmad-help.csv
|
|
1698
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
1699
|
+
*/
|
|
1700
|
+
async mergeModuleHelpCatalogs(bmadDir) {
|
|
1701
|
+
const allRows = [];
|
|
1702
|
+
const headerRow =
|
|
1703
|
+
'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs';
|
|
1704
|
+
|
|
1705
|
+
// Load agent manifest for agent info lookup
|
|
1706
|
+
const agentManifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
|
|
1707
|
+
const agentInfo = new Map(); // agent-name -> {command, displayName, title+icon}
|
|
1708
|
+
|
|
1709
|
+
if (await fs.pathExists(agentManifestPath)) {
|
|
1710
|
+
const manifestContent = await fs.readFile(agentManifestPath, 'utf8');
|
|
1711
|
+
const lines = manifestContent.split('\n').filter((line) => line.trim());
|
|
1712
|
+
|
|
1713
|
+
for (const line of lines) {
|
|
1714
|
+
if (line.startsWith('name,')) continue; // Skip header
|
|
1715
|
+
|
|
1716
|
+
const cols = line.split(',');
|
|
1717
|
+
if (cols.length >= 4) {
|
|
1718
|
+
const agentName = cols[0].replaceAll('"', '').trim();
|
|
1719
|
+
const displayName = cols[1].replaceAll('"', '').trim();
|
|
1720
|
+
const title = cols[2].replaceAll('"', '').trim();
|
|
1721
|
+
const icon = cols[3].replaceAll('"', '').trim();
|
|
1722
|
+
const module = cols[10] ? cols[10].replaceAll('"', '').trim() : '';
|
|
1723
|
+
|
|
1724
|
+
// Build agent command: bmad:module:agent:name
|
|
1725
|
+
const agentCommand = module ? `bmad:${module}:agent:${agentName}` : `bmad:agent:${agentName}`;
|
|
1726
|
+
|
|
1727
|
+
agentInfo.set(agentName, {
|
|
1728
|
+
command: agentCommand,
|
|
1729
|
+
displayName: displayName || agentName,
|
|
1730
|
+
title: icon && title ? `${icon} ${title}` : title || agentName,
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Get all installed module directories
|
|
1737
|
+
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
1738
|
+
const installedModules = entries
|
|
1739
|
+
.filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs' && entry.name !== '_memory')
|
|
1740
|
+
.map((entry) => entry.name);
|
|
1741
|
+
|
|
1742
|
+
// Add core module to scan (it's installed at root level as _config, but we check src/core)
|
|
1743
|
+
const coreModulePath = getSourcePath('core');
|
|
1744
|
+
const modulePaths = new Map();
|
|
1745
|
+
|
|
1746
|
+
// Map all module source paths
|
|
1747
|
+
if (await fs.pathExists(coreModulePath)) {
|
|
1748
|
+
modulePaths.set('core', coreModulePath);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// Map installed module paths
|
|
1752
|
+
for (const moduleName of installedModules) {
|
|
1753
|
+
const modulePath = path.join(bmadDir, moduleName);
|
|
1754
|
+
modulePaths.set(moduleName, modulePath);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// Scan each module for module-help.csv
|
|
1758
|
+
for (const [moduleName, modulePath] of modulePaths) {
|
|
1759
|
+
const helpFilePath = path.join(modulePath, 'module-help.csv');
|
|
1760
|
+
|
|
1761
|
+
if (await fs.pathExists(helpFilePath)) {
|
|
1762
|
+
try {
|
|
1763
|
+
const content = await fs.readFile(helpFilePath, 'utf8');
|
|
1764
|
+
const lines = content.split('\n').filter((line) => line.trim() && !line.startsWith('#'));
|
|
1765
|
+
|
|
1766
|
+
for (const line of lines) {
|
|
1767
|
+
// Skip header row
|
|
1768
|
+
if (line.startsWith('module,')) {
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// Parse the line - handle quoted fields with commas
|
|
1773
|
+
const columns = this.parseCSVLine(line);
|
|
1774
|
+
if (columns.length >= 12) {
|
|
1775
|
+
// Map old schema to new schema
|
|
1776
|
+
// Old: module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
|
|
1777
|
+
// New: module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs
|
|
1778
|
+
|
|
1779
|
+
const [
|
|
1780
|
+
module,
|
|
1781
|
+
phase,
|
|
1782
|
+
name,
|
|
1783
|
+
code,
|
|
1784
|
+
sequence,
|
|
1785
|
+
workflowFile,
|
|
1786
|
+
command,
|
|
1787
|
+
required,
|
|
1788
|
+
agentName,
|
|
1789
|
+
options,
|
|
1790
|
+
description,
|
|
1791
|
+
outputLocation,
|
|
1792
|
+
outputs,
|
|
1793
|
+
] = columns;
|
|
1794
|
+
|
|
1795
|
+
// If module column is empty, set it to this module's name (except for core which stays empty for universal tools)
|
|
1796
|
+
const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
|
|
1797
|
+
|
|
1798
|
+
// Lookup agent info
|
|
1799
|
+
const cleanAgentName = agentName ? agentName.trim() : '';
|
|
1800
|
+
const agentData = agentInfo.get(cleanAgentName) || { command: '', displayName: '', title: '' };
|
|
1801
|
+
|
|
1802
|
+
// Build new row with agent info
|
|
1803
|
+
const newRow = [
|
|
1804
|
+
finalModule,
|
|
1805
|
+
phase || '',
|
|
1806
|
+
name || '',
|
|
1807
|
+
code || '',
|
|
1808
|
+
sequence || '',
|
|
1809
|
+
workflowFile || '',
|
|
1810
|
+
command || '',
|
|
1811
|
+
required || 'false',
|
|
1812
|
+
cleanAgentName,
|
|
1813
|
+
agentData.command,
|
|
1814
|
+
agentData.displayName,
|
|
1815
|
+
agentData.title,
|
|
1816
|
+
options || '',
|
|
1817
|
+
description || '',
|
|
1818
|
+
outputLocation || '',
|
|
1819
|
+
outputs || '',
|
|
1820
|
+
];
|
|
1821
|
+
|
|
1822
|
+
allRows.push(newRow.map((c) => this.escapeCSVField(c)).join(','));
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
|
1827
|
+
await prompts.log.message(` Merged module-help from: ${moduleName}`);
|
|
1828
|
+
}
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
await prompts.log.warn(` Warning: Failed to read module-help.csv from ${moduleName}: ${error.message}`);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Sort by module, then phase, then sequence
|
|
1836
|
+
allRows.sort((a, b) => {
|
|
1837
|
+
const colsA = this.parseCSVLine(a);
|
|
1838
|
+
const colsB = this.parseCSVLine(b);
|
|
1839
|
+
|
|
1840
|
+
// Module comparison (empty module/universal tools come first)
|
|
1841
|
+
const moduleA = (colsA[0] || '').toLowerCase();
|
|
1842
|
+
const moduleB = (colsB[0] || '').toLowerCase();
|
|
1843
|
+
if (moduleA !== moduleB) {
|
|
1844
|
+
return moduleA.localeCompare(moduleB);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Phase comparison
|
|
1848
|
+
const phaseA = colsA[1] || '';
|
|
1849
|
+
const phaseB = colsB[1] || '';
|
|
1850
|
+
if (phaseA !== phaseB) {
|
|
1851
|
+
return phaseA.localeCompare(phaseB);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// Sequence comparison
|
|
1855
|
+
const seqA = parseInt(colsA[4] || '0', 10);
|
|
1856
|
+
const seqB = parseInt(colsB[4] || '0', 10);
|
|
1857
|
+
return seqA - seqB;
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
// Write merged catalog
|
|
1861
|
+
const outputDir = path.join(bmadDir, '_config');
|
|
1862
|
+
await fs.ensureDir(outputDir);
|
|
1863
|
+
const outputPath = path.join(outputDir, 'bmad-help.csv');
|
|
1864
|
+
|
|
1865
|
+
const mergedContent = [headerRow, ...allRows].join('\n');
|
|
1866
|
+
await fs.writeFile(outputPath, mergedContent, 'utf8');
|
|
1867
|
+
|
|
1868
|
+
// Track the installed file
|
|
1869
|
+
this.installedFiles.add(outputPath);
|
|
1870
|
+
|
|
1871
|
+
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
|
1872
|
+
await prompts.log.message(` Generated bmad-help.csv: ${allRows.length} workflows`);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* Parse a CSV line, handling quoted fields
|
|
1878
|
+
* @param {string} line - CSV line to parse
|
|
1879
|
+
* @returns {Array} Array of field values
|
|
1880
|
+
*/
|
|
1881
|
+
parseCSVLine(line) {
|
|
1882
|
+
const result = [];
|
|
1883
|
+
let current = '';
|
|
1884
|
+
let inQuotes = false;
|
|
1885
|
+
|
|
1886
|
+
for (let i = 0; i < line.length; i++) {
|
|
1887
|
+
const char = line[i];
|
|
1888
|
+
const nextChar = line[i + 1];
|
|
1889
|
+
|
|
1890
|
+
if (char === '"') {
|
|
1891
|
+
if (inQuotes && nextChar === '"') {
|
|
1892
|
+
// Escaped quote
|
|
1893
|
+
current += '"';
|
|
1894
|
+
i++; // Skip next quote
|
|
1895
|
+
} else {
|
|
1896
|
+
// Toggle quote mode
|
|
1897
|
+
inQuotes = !inQuotes;
|
|
1898
|
+
}
|
|
1899
|
+
} else if (char === ',' && !inQuotes) {
|
|
1900
|
+
result.push(current);
|
|
1901
|
+
current = '';
|
|
1902
|
+
} else {
|
|
1903
|
+
current += char;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
result.push(current);
|
|
1907
|
+
return result;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
/**
|
|
1911
|
+
* Escape a CSV field if it contains special characters
|
|
1912
|
+
* @param {string} field - Field value to escape
|
|
1913
|
+
* @returns {string} Escaped field
|
|
1914
|
+
*/
|
|
1915
|
+
escapeCSVField(field) {
|
|
1916
|
+
if (field === null || field === undefined) {
|
|
1917
|
+
return '';
|
|
1918
|
+
}
|
|
1919
|
+
const str = String(field);
|
|
1920
|
+
// If field contains comma, quote, or newline, wrap in quotes and escape inner quotes
|
|
1921
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
1922
|
+
return `"${str.replaceAll('"', '""')}"`;
|
|
1923
|
+
}
|
|
1924
|
+
return str;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
async createDirectoryStructure(bmadDir) {
|
|
1928
|
+
await fs.ensureDir(bmadDir);
|
|
1929
|
+
await fs.ensureDir(path.join(bmadDir, '_config'));
|
|
1930
|
+
await fs.ensureDir(path.join(bmadDir, '_config', 'agents'));
|
|
1931
|
+
await fs.ensureDir(path.join(bmadDir, '_config', 'custom'));
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
/**
|
|
1935
|
+
* Generate clean config.yaml files for each installed module
|
|
1936
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
1937
|
+
* @param {Object} moduleConfigs - Collected configuration values
|
|
1938
|
+
*/
|
|
1939
|
+
async generateModuleConfigs(bmadDir, moduleConfigs) {
|
|
1940
|
+
const yaml = require('yaml');
|
|
1941
|
+
|
|
1942
|
+
// Extract core config values to share with other modules
|
|
1943
|
+
const coreConfig = moduleConfigs.core || {};
|
|
1944
|
+
|
|
1945
|
+
// Get all installed module directories
|
|
1946
|
+
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
1947
|
+
const installedModules = entries
|
|
1948
|
+
.filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs')
|
|
1949
|
+
.map((entry) => entry.name);
|
|
1950
|
+
|
|
1951
|
+
// Generate config.yaml for each installed module
|
|
1952
|
+
for (const moduleName of installedModules) {
|
|
1953
|
+
const modulePath = path.join(bmadDir, moduleName);
|
|
1954
|
+
|
|
1955
|
+
// Get module-specific config or use empty object if none
|
|
1956
|
+
const config = moduleConfigs[moduleName] || {};
|
|
1957
|
+
|
|
1958
|
+
if (await fs.pathExists(modulePath)) {
|
|
1959
|
+
const configPath = path.join(modulePath, 'config.yaml');
|
|
1960
|
+
|
|
1961
|
+
// Create header
|
|
1962
|
+
const packageJson = require(path.join(getProjectRoot(), 'package.json'));
|
|
1963
|
+
const header = `# ${moduleName.toUpperCase()} Module Configuration
|
|
1964
|
+
# Generated by BMAD installer
|
|
1965
|
+
# Version: ${packageJson.version}
|
|
1966
|
+
# Date: ${new Date().toISOString()}
|
|
1967
|
+
|
|
1968
|
+
`;
|
|
1969
|
+
|
|
1970
|
+
// For non-core modules, add core config values directly
|
|
1971
|
+
let finalConfig = { ...config };
|
|
1972
|
+
let coreSection = '';
|
|
1973
|
+
|
|
1974
|
+
if (moduleName !== 'core' && coreConfig && Object.keys(coreConfig).length > 0) {
|
|
1975
|
+
// Add core values directly to the module config
|
|
1976
|
+
// These will be available for reference in the module
|
|
1977
|
+
finalConfig = {
|
|
1978
|
+
...config,
|
|
1979
|
+
...coreConfig, // Spread core config values directly into the module config
|
|
1980
|
+
};
|
|
1981
|
+
|
|
1982
|
+
// Create a comment section to identify core values
|
|
1983
|
+
coreSection = '\n# Core Configuration Values\n';
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// Clean the config to remove any non-serializable values (like functions)
|
|
1987
|
+
const cleanConfig = structuredClone(finalConfig);
|
|
1988
|
+
|
|
1989
|
+
// Convert config to YAML
|
|
1990
|
+
let yamlContent = yaml.stringify(cleanConfig, {
|
|
1991
|
+
indent: 2,
|
|
1992
|
+
lineWidth: 0,
|
|
1993
|
+
minContentWidth: 0,
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
// If we have core values, reorganize the YAML to group them with their comment
|
|
1997
|
+
if (coreSection && moduleName !== 'core') {
|
|
1998
|
+
// Split the YAML into lines
|
|
1999
|
+
const lines = yamlContent.split('\n');
|
|
2000
|
+
const moduleConfigLines = [];
|
|
2001
|
+
const coreConfigLines = [];
|
|
2002
|
+
|
|
2003
|
+
// Separate module-specific and core config lines
|
|
2004
|
+
for (const line of lines) {
|
|
2005
|
+
const key = line.split(':')[0].trim();
|
|
2006
|
+
if (Object.prototype.hasOwnProperty.call(coreConfig, key)) {
|
|
2007
|
+
coreConfigLines.push(line);
|
|
2008
|
+
} else {
|
|
2009
|
+
moduleConfigLines.push(line);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Rebuild YAML with module config first, then core config with comment
|
|
2014
|
+
yamlContent = moduleConfigLines.join('\n');
|
|
2015
|
+
if (coreConfigLines.length > 0) {
|
|
2016
|
+
yamlContent += coreSection + coreConfigLines.join('\n');
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// Write the clean config file with POSIX-compliant final newline
|
|
2021
|
+
const content = header + yamlContent;
|
|
2022
|
+
await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8');
|
|
2023
|
+
|
|
2024
|
+
// Track the config file in installedFiles
|
|
2025
|
+
this.installedFiles.add(configPath);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* Install core with resolved dependencies
|
|
2032
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
2033
|
+
* @param {Object} coreFiles - Core files to install
|
|
2034
|
+
*/
|
|
2035
|
+
async installCoreWithDependencies(bmadDir, coreFiles) {
|
|
2036
|
+
const sourcePath = getModulePath('core');
|
|
2037
|
+
const targetPath = path.join(bmadDir, 'core');
|
|
2038
|
+
await this.installCore(bmadDir);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* Install module with resolved dependencies
|
|
2043
|
+
* @param {string} moduleName - Module name
|
|
2044
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
2045
|
+
* @param {Object} moduleFiles - Module files to install
|
|
2046
|
+
*/
|
|
2047
|
+
async installModuleWithDependencies(moduleName, bmadDir, moduleFiles) {
|
|
2048
|
+
// Get module configuration for conditional installation
|
|
2049
|
+
const moduleConfig = this.configCollector.collectedConfig[moduleName] || {};
|
|
2050
|
+
|
|
2051
|
+
// Use existing module manager for full installation with file tracking
|
|
2052
|
+
// Note: Module-specific installers are called separately after IDE setup
|
|
2053
|
+
await this.moduleManager.install(
|
|
2054
|
+
moduleName,
|
|
2055
|
+
bmadDir,
|
|
2056
|
+
(filePath) => {
|
|
2057
|
+
this.installedFiles.add(filePath);
|
|
2058
|
+
},
|
|
2059
|
+
{
|
|
2060
|
+
skipModuleInstaller: true, // We'll run it later after IDE setup
|
|
2061
|
+
moduleConfig: moduleConfig, // Pass module config for conditional filtering
|
|
2062
|
+
installer: this,
|
|
2063
|
+
silent: true,
|
|
2064
|
+
},
|
|
2065
|
+
);
|
|
2066
|
+
|
|
2067
|
+
// Process agent files to build YAML agents and create customize templates
|
|
2068
|
+
const modulePath = path.join(bmadDir, moduleName);
|
|
2069
|
+
await this.processAgentFiles(modulePath, moduleName);
|
|
2070
|
+
|
|
2071
|
+
// Dependencies are already included in full module install
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
/**
|
|
2075
|
+
* Install partial module (only dependencies needed by other modules)
|
|
2076
|
+
*/
|
|
2077
|
+
async installPartialModule(moduleName, bmadDir, files) {
|
|
2078
|
+
const sourceBase = getModulePath(moduleName);
|
|
2079
|
+
const targetBase = path.join(bmadDir, moduleName);
|
|
2080
|
+
|
|
2081
|
+
// Create module directory
|
|
2082
|
+
await fs.ensureDir(targetBase);
|
|
2083
|
+
|
|
2084
|
+
// Copy only the required dependency files
|
|
2085
|
+
if (files.agents && files.agents.length > 0) {
|
|
2086
|
+
const agentsDir = path.join(targetBase, 'agents');
|
|
2087
|
+
await fs.ensureDir(agentsDir);
|
|
2088
|
+
|
|
2089
|
+
for (const agentPath of files.agents) {
|
|
2090
|
+
const fileName = path.basename(agentPath);
|
|
2091
|
+
const sourcePath = path.join(sourceBase, 'agents', fileName);
|
|
2092
|
+
const targetPath = path.join(agentsDir, fileName);
|
|
2093
|
+
|
|
2094
|
+
if (await fs.pathExists(sourcePath)) {
|
|
2095
|
+
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
|
2096
|
+
this.installedFiles.add(targetPath);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
if (files.tasks && files.tasks.length > 0) {
|
|
2102
|
+
const tasksDir = path.join(targetBase, 'tasks');
|
|
2103
|
+
await fs.ensureDir(tasksDir);
|
|
2104
|
+
|
|
2105
|
+
for (const taskPath of files.tasks) {
|
|
2106
|
+
const fileName = path.basename(taskPath);
|
|
2107
|
+
const sourcePath = path.join(sourceBase, 'tasks', fileName);
|
|
2108
|
+
const targetPath = path.join(tasksDir, fileName);
|
|
2109
|
+
|
|
2110
|
+
if (await fs.pathExists(sourcePath)) {
|
|
2111
|
+
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
|
2112
|
+
this.installedFiles.add(targetPath);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
if (files.tools && files.tools.length > 0) {
|
|
2118
|
+
const toolsDir = path.join(targetBase, 'tools');
|
|
2119
|
+
await fs.ensureDir(toolsDir);
|
|
2120
|
+
|
|
2121
|
+
for (const toolPath of files.tools) {
|
|
2122
|
+
const fileName = path.basename(toolPath);
|
|
2123
|
+
const sourcePath = path.join(sourceBase, 'tools', fileName);
|
|
2124
|
+
const targetPath = path.join(toolsDir, fileName);
|
|
2125
|
+
|
|
2126
|
+
if (await fs.pathExists(sourcePath)) {
|
|
2127
|
+
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
|
2128
|
+
this.installedFiles.add(targetPath);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
if (files.templates && files.templates.length > 0) {
|
|
2134
|
+
const templatesDir = path.join(targetBase, 'templates');
|
|
2135
|
+
await fs.ensureDir(templatesDir);
|
|
2136
|
+
|
|
2137
|
+
for (const templatePath of files.templates) {
|
|
2138
|
+
const fileName = path.basename(templatePath);
|
|
2139
|
+
const sourcePath = path.join(sourceBase, 'templates', fileName);
|
|
2140
|
+
const targetPath = path.join(templatesDir, fileName);
|
|
2141
|
+
|
|
2142
|
+
if (await fs.pathExists(sourcePath)) {
|
|
2143
|
+
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
|
2144
|
+
this.installedFiles.add(targetPath);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
if (files.data && files.data.length > 0) {
|
|
2150
|
+
for (const dataPath of files.data) {
|
|
2151
|
+
// Preserve directory structure for data files
|
|
2152
|
+
const relative = path.relative(sourceBase, dataPath);
|
|
2153
|
+
const targetPath = path.join(targetBase, relative);
|
|
2154
|
+
|
|
2155
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
2156
|
+
|
|
2157
|
+
if (await fs.pathExists(dataPath)) {
|
|
2158
|
+
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath);
|
|
2159
|
+
this.installedFiles.add(targetPath);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// Create a marker file to indicate this is a partial installation
|
|
2165
|
+
const markerPath = path.join(targetBase, '.partial');
|
|
2166
|
+
await fs.writeFile(
|
|
2167
|
+
markerPath,
|
|
2168
|
+
`This module contains only dependencies required by other modules.\nInstalled: ${new Date().toISOString()}\n`,
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Private: Install core
|
|
2174
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
2175
|
+
*/
|
|
2176
|
+
async installCore(bmadDir) {
|
|
2177
|
+
const sourcePath = getModulePath('core');
|
|
2178
|
+
const targetPath = path.join(bmadDir, 'core');
|
|
2179
|
+
|
|
2180
|
+
// Copy core files (skip .agent.yaml files like modules do)
|
|
2181
|
+
await this.copyCoreFiles(sourcePath, targetPath);
|
|
2182
|
+
|
|
2183
|
+
// Compile agents using the same compiler as modules
|
|
2184
|
+
const { ModuleManager } = require('../modules/manager');
|
|
2185
|
+
const moduleManager = new ModuleManager();
|
|
2186
|
+
await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir, this);
|
|
2187
|
+
|
|
2188
|
+
// Process agent files to inject activation block
|
|
2189
|
+
await this.processAgentFiles(targetPath, 'core');
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/**
|
|
2193
|
+
* Copy core files (similar to copyModuleWithFiltering but for core)
|
|
2194
|
+
* @param {string} sourcePath - Source path
|
|
2195
|
+
* @param {string} targetPath - Target path
|
|
2196
|
+
*/
|
|
2197
|
+
async copyCoreFiles(sourcePath, targetPath) {
|
|
2198
|
+
// Get all files in source
|
|
2199
|
+
const files = await this.getFileList(sourcePath);
|
|
2200
|
+
|
|
2201
|
+
for (const file of files) {
|
|
2202
|
+
// Skip sub-modules directory - these are IDE-specific and handled separately
|
|
2203
|
+
if (file.startsWith('sub-modules/')) {
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// Skip sidecar directories - they are handled separately during agent compilation
|
|
2208
|
+
if (
|
|
2209
|
+
path
|
|
2210
|
+
.dirname(file)
|
|
2211
|
+
.split('/')
|
|
2212
|
+
.some((dir) => dir.toLowerCase().includes('sidecar'))
|
|
2213
|
+
) {
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// Skip module.yaml at root - it's only needed at install time
|
|
2218
|
+
if (file === 'module.yaml') {
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// Skip config.yaml templates - we'll generate clean ones with actual values
|
|
2223
|
+
if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) {
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
// Skip .agent.yaml files - they will be compiled separately
|
|
2228
|
+
if (file.endsWith('.agent.yaml')) {
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
const sourceFile = path.join(sourcePath, file);
|
|
2233
|
+
const targetFile = path.join(targetPath, file);
|
|
2234
|
+
|
|
2235
|
+
// Check if this is an agent file
|
|
2236
|
+
if (file.startsWith('agents/') && file.endsWith('.md')) {
|
|
2237
|
+
// Read the file to check for localskip
|
|
2238
|
+
const content = await fs.readFile(sourceFile, 'utf8');
|
|
2239
|
+
|
|
2240
|
+
// Check for localskip="true" in the agent tag
|
|
2241
|
+
const agentMatch = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
|
|
2242
|
+
if (agentMatch) {
|
|
2243
|
+
await prompts.log.message(` Skipping web-only agent: ${path.basename(file)}`);
|
|
2244
|
+
continue; // Skip this agent
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// Copy the file with placeholder replacement
|
|
2249
|
+
await fs.ensureDir(path.dirname(targetFile));
|
|
2250
|
+
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
|
|
2251
|
+
|
|
2252
|
+
// Track the installed file
|
|
2253
|
+
this.installedFiles.add(targetFile);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/**
|
|
2258
|
+
* Get list of all files in a directory recursively
|
|
2259
|
+
* @param {string} dir - Directory path
|
|
2260
|
+
* @param {string} baseDir - Base directory for relative paths
|
|
2261
|
+
* @returns {Array} List of relative file paths
|
|
2262
|
+
*/
|
|
2263
|
+
async getFileList(dir, baseDir = dir) {
|
|
2264
|
+
const files = [];
|
|
2265
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2266
|
+
|
|
2267
|
+
for (const entry of entries) {
|
|
2268
|
+
const fullPath = path.join(dir, entry.name);
|
|
2269
|
+
|
|
2270
|
+
if (entry.isDirectory()) {
|
|
2271
|
+
const subFiles = await this.getFileList(fullPath, baseDir);
|
|
2272
|
+
files.push(...subFiles);
|
|
2273
|
+
} else {
|
|
2274
|
+
files.push(path.relative(baseDir, fullPath));
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
return files;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* Process agent files to build YAML agents and inject activation blocks
|
|
2283
|
+
* @param {string} modulePath - Path to module in bmad/ installation
|
|
2284
|
+
* @param {string} moduleName - Module name
|
|
2285
|
+
*/
|
|
2286
|
+
async processAgentFiles(modulePath, moduleName) {
|
|
2287
|
+
const agentsPath = path.join(modulePath, 'agents');
|
|
2288
|
+
|
|
2289
|
+
// Check if agents directory exists
|
|
2290
|
+
if (!(await fs.pathExists(agentsPath))) {
|
|
2291
|
+
return; // No agents to process
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// Determine project directory (parent of bmad/ directory)
|
|
2295
|
+
const bmadDir = path.dirname(modulePath);
|
|
2296
|
+
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
|
|
2297
|
+
|
|
2298
|
+
// Ensure _config/agents directory exists
|
|
2299
|
+
await fs.ensureDir(cfgAgentsDir);
|
|
2300
|
+
|
|
2301
|
+
// Get all agent files
|
|
2302
|
+
const agentFiles = await fs.readdir(agentsPath);
|
|
2303
|
+
|
|
2304
|
+
for (const agentFile of agentFiles) {
|
|
2305
|
+
// Skip .agent.yaml files - they should already be compiled by compileModuleAgents
|
|
2306
|
+
if (agentFile.endsWith('.agent.yaml')) {
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// Only process .md files (already compiled from YAML)
|
|
2311
|
+
if (!agentFile.endsWith('.md')) {
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
const agentName = agentFile.replace('.md', '');
|
|
2316
|
+
const mdPath = path.join(agentsPath, agentFile);
|
|
2317
|
+
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
|
|
2318
|
+
|
|
2319
|
+
// For .md files that are already compiled, we don't need to do much
|
|
2320
|
+
// Just ensure the customize template exists
|
|
2321
|
+
if (!(await fs.pathExists(customizePath))) {
|
|
2322
|
+
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
|
|
2323
|
+
if (await fs.pathExists(genericTemplatePath)) {
|
|
2324
|
+
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
|
|
2325
|
+
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
|
2326
|
+
await prompts.log.message(` Created customize: ${moduleName}-${agentName}.customize.yaml`);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
/**
|
|
2334
|
+
* Private: Update core
|
|
2335
|
+
*/
|
|
2336
|
+
async updateCore(bmadDir, force = false) {
|
|
2337
|
+
const sourcePath = getModulePath('core');
|
|
2338
|
+
const targetPath = path.join(bmadDir, 'core');
|
|
2339
|
+
|
|
2340
|
+
if (force) {
|
|
2341
|
+
await fs.remove(targetPath);
|
|
2342
|
+
await this.installCore(bmadDir);
|
|
2343
|
+
} else {
|
|
2344
|
+
// Selective update - preserve user modifications
|
|
2345
|
+
await this.fileOps.syncDirectory(sourcePath, targetPath);
|
|
2346
|
+
|
|
2347
|
+
// Recompile agents (#1133)
|
|
2348
|
+
const { ModuleManager } = require('../modules/manager');
|
|
2349
|
+
const moduleManager = new ModuleManager();
|
|
2350
|
+
await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir, this);
|
|
2351
|
+
await this.processAgentFiles(targetPath, 'core');
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
/**
|
|
2356
|
+
* Quick update method - preserves all settings and only prompts for new config fields
|
|
2357
|
+
* @param {Object} config - Configuration with directory
|
|
2358
|
+
* @returns {Object} Update result
|
|
2359
|
+
*/
|
|
2360
|
+
async quickUpdate(config) {
|
|
2361
|
+
const spinner = await prompts.spinner();
|
|
2362
|
+
spinner.start('Starting quick update...');
|
|
2363
|
+
|
|
2364
|
+
try {
|
|
2365
|
+
const projectDir = path.resolve(config.directory);
|
|
2366
|
+
const { bmadDir } = await this.findBmadDir(projectDir);
|
|
2367
|
+
|
|
2368
|
+
// Check if bmad directory exists
|
|
2369
|
+
if (!(await fs.pathExists(bmadDir))) {
|
|
2370
|
+
spinner.stop('No BMAD installation found');
|
|
2371
|
+
throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
spinner.message('Detecting installed modules and configuration...');
|
|
2375
|
+
|
|
2376
|
+
// Detect existing installation
|
|
2377
|
+
const existingInstall = await this.detector.detect(bmadDir);
|
|
2378
|
+
const installedModules = existingInstall.modules.map((m) => m.id);
|
|
2379
|
+
const configuredIdes = existingInstall.ides || [];
|
|
2380
|
+
const projectRoot = path.dirname(bmadDir);
|
|
2381
|
+
|
|
2382
|
+
// Get custom module sources: first from --custom-content (re-cache from source), then from cache
|
|
2383
|
+
const customModuleSources = new Map();
|
|
2384
|
+
if (config.customContent?.sources?.length > 0) {
|
|
2385
|
+
for (const source of config.customContent.sources) {
|
|
2386
|
+
if (source.id && source.path && (await fs.pathExists(source.path))) {
|
|
2387
|
+
customModuleSources.set(source.id, {
|
|
2388
|
+
id: source.id,
|
|
2389
|
+
name: source.name || source.id,
|
|
2390
|
+
sourcePath: source.path,
|
|
2391
|
+
cached: false, // From CLI, will be re-cached
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
2397
|
+
if (await fs.pathExists(cacheDir)) {
|
|
2398
|
+
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
2399
|
+
|
|
2400
|
+
for (const cachedModule of cachedModules) {
|
|
2401
|
+
const moduleId = cachedModule.name;
|
|
2402
|
+
const cachedPath = path.join(cacheDir, moduleId);
|
|
2403
|
+
|
|
2404
|
+
// Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
|
2405
|
+
if (!(await fs.pathExists(cachedPath))) {
|
|
2406
|
+
continue;
|
|
2407
|
+
}
|
|
2408
|
+
if (!cachedModule.isDirectory()) {
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// Skip if we already have this module from manifest
|
|
2413
|
+
if (customModuleSources.has(moduleId)) {
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
// Check if this is an external official module - skip cache for those
|
|
2418
|
+
const isExternal = await this.moduleManager.isExternalModule(moduleId);
|
|
2419
|
+
if (isExternal) {
|
|
2420
|
+
// External modules are handled via cloneExternalModule, not from cache
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// Check if this is actually a custom module (has module.yaml)
|
|
2425
|
+
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
|
2426
|
+
if (await fs.pathExists(moduleYamlPath)) {
|
|
2427
|
+
// For quick update, we always rebuild from cache
|
|
2428
|
+
customModuleSources.set(moduleId, {
|
|
2429
|
+
id: moduleId,
|
|
2430
|
+
name: moduleId, // We'll read the actual name if needed
|
|
2431
|
+
sourcePath: cachedPath,
|
|
2432
|
+
cached: true, // Flag to indicate this is from cache
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// Load saved IDE configurations
|
|
2439
|
+
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
|
2440
|
+
|
|
2441
|
+
// Get available modules (what we have source for)
|
|
2442
|
+
const availableModulesData = await this.moduleManager.listAvailable();
|
|
2443
|
+
const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
|
|
2444
|
+
|
|
2445
|
+
// Add external official modules to available modules
|
|
2446
|
+
// These can always be obtained by cloning from their remote URLs
|
|
2447
|
+
const { ExternalModuleManager } = require('../modules/external-manager');
|
|
2448
|
+
const externalManager = new ExternalModuleManager();
|
|
2449
|
+
const externalModules = await externalManager.listAvailable();
|
|
2450
|
+
for (const externalModule of externalModules) {
|
|
2451
|
+
// Only add if not already in the list and is installed
|
|
2452
|
+
if (installedModules.includes(externalModule.code) && !availableModules.some((m) => m.id === externalModule.code)) {
|
|
2453
|
+
availableModules.push({
|
|
2454
|
+
id: externalModule.code,
|
|
2455
|
+
name: externalModule.name,
|
|
2456
|
+
isExternal: true,
|
|
2457
|
+
fromExternal: true,
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// Add custom modules from manifest if their sources exist
|
|
2463
|
+
for (const [moduleId, customModule] of customModuleSources) {
|
|
2464
|
+
// Use the absolute sourcePath
|
|
2465
|
+
const sourcePath = customModule.sourcePath;
|
|
2466
|
+
|
|
2467
|
+
// Check if source exists at the recorded path
|
|
2468
|
+
if (
|
|
2469
|
+
sourcePath &&
|
|
2470
|
+
(await fs.pathExists(sourcePath)) && // Add to available modules if not already there
|
|
2471
|
+
!availableModules.some((m) => m.id === moduleId)
|
|
2472
|
+
) {
|
|
2473
|
+
availableModules.push({
|
|
2474
|
+
id: moduleId,
|
|
2475
|
+
name: customModule.name || moduleId,
|
|
2476
|
+
path: sourcePath,
|
|
2477
|
+
isCustom: true,
|
|
2478
|
+
fromManifest: true,
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
// Handle missing custom module sources using shared method
|
|
2484
|
+
const customModuleResult = await this.handleMissingCustomSources(
|
|
2485
|
+
customModuleSources,
|
|
2486
|
+
bmadDir,
|
|
2487
|
+
projectRoot,
|
|
2488
|
+
'update',
|
|
2489
|
+
installedModules,
|
|
2490
|
+
config.skipPrompts || false,
|
|
2491
|
+
);
|
|
2492
|
+
|
|
2493
|
+
const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
|
|
2494
|
+
|
|
2495
|
+
const customModulesFromManifest = validCustomModules.map((m) => ({
|
|
2496
|
+
...m,
|
|
2497
|
+
isCustom: true,
|
|
2498
|
+
hasUpdate: true,
|
|
2499
|
+
}));
|
|
2500
|
+
|
|
2501
|
+
const allAvailableModules = [...availableModules, ...customModulesFromManifest];
|
|
2502
|
+
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
|
|
2503
|
+
|
|
2504
|
+
// Core module is special - never include it in update flow
|
|
2505
|
+
const nonCoreInstalledModules = installedModules.filter((id) => id !== 'core');
|
|
2506
|
+
|
|
2507
|
+
// Only update modules that are BOTH installed AND available (we have source for)
|
|
2508
|
+
const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id));
|
|
2509
|
+
const skippedModules = nonCoreInstalledModules.filter((id) => !availableModuleIds.has(id));
|
|
2510
|
+
|
|
2511
|
+
// Add custom modules that were kept without sources to the skipped modules
|
|
2512
|
+
// This ensures their agents are preserved in the manifest
|
|
2513
|
+
for (const keptModule of keptModulesWithoutSources) {
|
|
2514
|
+
if (!skippedModules.includes(keptModule)) {
|
|
2515
|
+
skippedModules.push(keptModule);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
spinner.stop(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`);
|
|
2520
|
+
|
|
2521
|
+
if (skippedModules.length > 0) {
|
|
2522
|
+
await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// Load existing configs and collect new fields (if any)
|
|
2526
|
+
await prompts.log.info('Checking for new configuration options...');
|
|
2527
|
+
await this.configCollector.loadExistingConfig(projectDir);
|
|
2528
|
+
|
|
2529
|
+
let promptedForNewFields = false;
|
|
2530
|
+
|
|
2531
|
+
// Check core config for new fields
|
|
2532
|
+
const corePrompted = await this.configCollector.collectModuleConfigQuick('core', projectDir, true);
|
|
2533
|
+
if (corePrompted) {
|
|
2534
|
+
promptedForNewFields = true;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// Check each module we're updating for new fields (NOT skipped modules)
|
|
2538
|
+
for (const moduleName of modulesToUpdate) {
|
|
2539
|
+
const modulePrompted = await this.configCollector.collectModuleConfigQuick(moduleName, projectDir, true);
|
|
2540
|
+
if (modulePrompted) {
|
|
2541
|
+
promptedForNewFields = true;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
if (!promptedForNewFields) {
|
|
2546
|
+
await prompts.log.success('All configuration is up to date, no new options to configure');
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// Add metadata
|
|
2550
|
+
this.configCollector.collectedConfig._meta = {
|
|
2551
|
+
version: require(path.join(getProjectRoot(), 'package.json')).version,
|
|
2552
|
+
installDate: new Date().toISOString(),
|
|
2553
|
+
lastModified: new Date().toISOString(),
|
|
2554
|
+
};
|
|
2555
|
+
|
|
2556
|
+
// Build the config object for the installer
|
|
2557
|
+
const installConfig = {
|
|
2558
|
+
directory: projectDir,
|
|
2559
|
+
installCore: true,
|
|
2560
|
+
modules: modulesToUpdate, // Only update modules we have source for
|
|
2561
|
+
ides: configuredIdes,
|
|
2562
|
+
skipIde: configuredIdes.length === 0,
|
|
2563
|
+
coreConfig: this.configCollector.collectedConfig.core,
|
|
2564
|
+
actionType: 'install', // Use regular install flow
|
|
2565
|
+
_quickUpdate: true, // Flag to skip certain prompts
|
|
2566
|
+
_preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
|
|
2567
|
+
_savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer
|
|
2568
|
+
_customModuleSources: customModuleSources, // Pass custom module sources for updates
|
|
2569
|
+
_existingModules: installedModules, // Pass all installed modules for manifest generation
|
|
2570
|
+
customContent: config.customContent, // Pass through for re-caching from source
|
|
2571
|
+
};
|
|
2572
|
+
|
|
2573
|
+
// Call the standard install method
|
|
2574
|
+
const result = await this.install(installConfig);
|
|
2575
|
+
|
|
2576
|
+
// Only succeed the spinner if it's still spinning
|
|
2577
|
+
// (install method might have stopped it if folder name changed)
|
|
2578
|
+
if (spinner.isSpinning) {
|
|
2579
|
+
spinner.stop('Quick update complete!');
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
return {
|
|
2583
|
+
success: true,
|
|
2584
|
+
moduleCount: modulesToUpdate.length + 1, // +1 for core
|
|
2585
|
+
hadNewFields: promptedForNewFields,
|
|
2586
|
+
modules: ['core', ...modulesToUpdate],
|
|
2587
|
+
skippedModules: skippedModules,
|
|
2588
|
+
ides: configuredIdes,
|
|
2589
|
+
};
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
spinner.error('Quick update failed');
|
|
2592
|
+
throw error;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
/**
|
|
2597
|
+
* Compile agents with customizations only
|
|
2598
|
+
* @param {Object} config - Configuration with directory
|
|
2599
|
+
* @returns {Object} Compilation result
|
|
2600
|
+
*/
|
|
2601
|
+
async compileAgents(config) {
|
|
2602
|
+
// Using @clack prompts
|
|
2603
|
+
const { ModuleManager } = require('../modules/manager');
|
|
2604
|
+
const { getSourcePath } = require('../../../lib/project-root');
|
|
2605
|
+
|
|
2606
|
+
const spinner = await prompts.spinner();
|
|
2607
|
+
spinner.start('Recompiling agents with customizations...');
|
|
2608
|
+
|
|
2609
|
+
try {
|
|
2610
|
+
const projectDir = path.resolve(config.directory);
|
|
2611
|
+
const { bmadDir } = await this.findBmadDir(projectDir);
|
|
2612
|
+
|
|
2613
|
+
// Check if bmad directory exists
|
|
2614
|
+
if (!(await fs.pathExists(bmadDir))) {
|
|
2615
|
+
spinner.stop('No BMAD installation found');
|
|
2616
|
+
throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// Detect existing installation
|
|
2620
|
+
const existingInstall = await this.detector.detect(bmadDir);
|
|
2621
|
+
const installedModules = existingInstall.modules.map((m) => m.id);
|
|
2622
|
+
|
|
2623
|
+
// Initialize module manager
|
|
2624
|
+
const moduleManager = new ModuleManager();
|
|
2625
|
+
moduleManager.setBmadFolderName(path.basename(bmadDir));
|
|
2626
|
+
|
|
2627
|
+
let totalAgentCount = 0;
|
|
2628
|
+
|
|
2629
|
+
// Get custom module sources from cache
|
|
2630
|
+
const customModuleSources = new Map();
|
|
2631
|
+
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
2632
|
+
if (await fs.pathExists(cacheDir)) {
|
|
2633
|
+
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
2634
|
+
|
|
2635
|
+
for (const cachedModule of cachedModules) {
|
|
2636
|
+
if (cachedModule.isDirectory()) {
|
|
2637
|
+
const moduleId = cachedModule.name;
|
|
2638
|
+
const cachedPath = path.join(cacheDir, moduleId);
|
|
2639
|
+
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
|
2640
|
+
|
|
2641
|
+
// Check if this is actually a custom module
|
|
2642
|
+
if (await fs.pathExists(moduleYamlPath)) {
|
|
2643
|
+
// Check if this is an external official module - skip cache for those
|
|
2644
|
+
const isExternal = await this.moduleManager.isExternalModule(moduleId);
|
|
2645
|
+
if (isExternal) {
|
|
2646
|
+
// External modules are handled via cloneExternalModule, not from cache
|
|
2647
|
+
continue;
|
|
2648
|
+
}
|
|
2649
|
+
customModuleSources.set(moduleId, cachedPath);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// Process each installed module
|
|
2656
|
+
for (const moduleId of installedModules) {
|
|
2657
|
+
spinner.message(`Recompiling agents in ${moduleId}...`);
|
|
2658
|
+
|
|
2659
|
+
// Get source path
|
|
2660
|
+
let sourcePath;
|
|
2661
|
+
if (moduleId === 'core') {
|
|
2662
|
+
sourcePath = getSourcePath('core');
|
|
2663
|
+
} else {
|
|
2664
|
+
// First check if it's in the custom cache
|
|
2665
|
+
if (customModuleSources.has(moduleId)) {
|
|
2666
|
+
sourcePath = customModuleSources.get(moduleId);
|
|
2667
|
+
} else {
|
|
2668
|
+
sourcePath = await moduleManager.findModuleSource(moduleId);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
if (!sourcePath) {
|
|
2673
|
+
await prompts.log.warn(`Source not found for module ${moduleId}, skipping...`);
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
const targetPath = path.join(bmadDir, moduleId);
|
|
2678
|
+
|
|
2679
|
+
// Compile agents for this module
|
|
2680
|
+
await moduleManager.compileModuleAgents(sourcePath, targetPath, moduleId, bmadDir, this);
|
|
2681
|
+
|
|
2682
|
+
// Count agents (rough estimate based on files)
|
|
2683
|
+
const agentsPath = path.join(targetPath, 'agents');
|
|
2684
|
+
if (await fs.pathExists(agentsPath)) {
|
|
2685
|
+
const agentFiles = await fs.readdir(agentsPath);
|
|
2686
|
+
const agentCount = agentFiles.filter((f) => f.endsWith('.md')).length;
|
|
2687
|
+
totalAgentCount += agentCount;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
spinner.stop('Agent recompilation complete!');
|
|
2692
|
+
|
|
2693
|
+
return {
|
|
2694
|
+
success: true,
|
|
2695
|
+
agentCount: totalAgentCount,
|
|
2696
|
+
modules: installedModules,
|
|
2697
|
+
};
|
|
2698
|
+
} catch (error) {
|
|
2699
|
+
spinner.error('Agent recompilation failed');
|
|
2700
|
+
throw error;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
/**
|
|
2705
|
+
* Private: Prompt for update action
|
|
2706
|
+
*/
|
|
2707
|
+
async promptUpdateAction() {
|
|
2708
|
+
const action = await prompts.select({
|
|
2709
|
+
message: 'What would you like to do?',
|
|
2710
|
+
choices: [{ name: 'Update existing installation', value: 'update' }],
|
|
2711
|
+
});
|
|
2712
|
+
return { action };
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
/**
|
|
2716
|
+
* Handle legacy BMAD v4 detection with simple warning
|
|
2717
|
+
* @param {string} _projectDir - Project directory (unused in simplified version)
|
|
2718
|
+
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
|
2719
|
+
*/
|
|
2720
|
+
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
|
2721
|
+
await prompts.note(
|
|
2722
|
+
'Found .bmad-method folder from BMAD v4 installation.\n\n' +
|
|
2723
|
+
'Before continuing with installation, we recommend:\n' +
|
|
2724
|
+
' 1. Remove the .bmad-method folder, OR\n' +
|
|
2725
|
+
' 2. Back it up by renaming it to another name (e.g., bmad-method-backup)\n\n' +
|
|
2726
|
+
'If your v4 installation set up rules or commands, you should remove those as well.',
|
|
2727
|
+
'Legacy BMAD v4 detected',
|
|
2728
|
+
);
|
|
2729
|
+
|
|
2730
|
+
const proceed = await prompts.select({
|
|
2731
|
+
message: 'What would you like to do?',
|
|
2732
|
+
choices: [
|
|
2733
|
+
{
|
|
2734
|
+
name: 'Exit and clean up manually (recommended)',
|
|
2735
|
+
value: 'exit',
|
|
2736
|
+
hint: 'Exit installation',
|
|
2737
|
+
},
|
|
2738
|
+
{
|
|
2739
|
+
name: 'Continue with installation anyway',
|
|
2740
|
+
value: 'continue',
|
|
2741
|
+
hint: 'Continue',
|
|
2742
|
+
},
|
|
2743
|
+
],
|
|
2744
|
+
default: 'exit',
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
if (proceed === 'exit') {
|
|
2748
|
+
await prompts.log.info('Please remove the .bmad-method folder and any v4 rules/commands, then run the installer again.');
|
|
2749
|
+
// Allow event loop to flush pending I/O before exit
|
|
2750
|
+
setImmediate(() => process.exit(0));
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
await prompts.log.warn('Proceeding with installation despite legacy v4 folder');
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
/**
|
|
2758
|
+
* Read files-manifest.csv
|
|
2759
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
2760
|
+
* @returns {Array} Array of file entries from files-manifest.csv
|
|
2761
|
+
*/
|
|
2762
|
+
async readFilesManifest(bmadDir) {
|
|
2763
|
+
const filesManifestPath = path.join(bmadDir, '_config', 'files-manifest.csv');
|
|
2764
|
+
if (!(await fs.pathExists(filesManifestPath))) {
|
|
2765
|
+
return [];
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
try {
|
|
2769
|
+
const content = await fs.readFile(filesManifestPath, 'utf8');
|
|
2770
|
+
const lines = content.split('\n');
|
|
2771
|
+
const files = [];
|
|
2772
|
+
|
|
2773
|
+
for (let i = 1; i < lines.length; i++) {
|
|
2774
|
+
// Skip header
|
|
2775
|
+
const line = lines[i].trim();
|
|
2776
|
+
if (!line) continue;
|
|
2777
|
+
|
|
2778
|
+
// Parse CSV line properly handling quoted values
|
|
2779
|
+
const parts = [];
|
|
2780
|
+
let current = '';
|
|
2781
|
+
let inQuotes = false;
|
|
2782
|
+
|
|
2783
|
+
for (const char of line) {
|
|
2784
|
+
if (char === '"') {
|
|
2785
|
+
inQuotes = !inQuotes;
|
|
2786
|
+
} else if (char === ',' && !inQuotes) {
|
|
2787
|
+
parts.push(current);
|
|
2788
|
+
current = '';
|
|
2789
|
+
} else {
|
|
2790
|
+
current += char;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
parts.push(current); // Add last part
|
|
2794
|
+
|
|
2795
|
+
if (parts.length >= 4) {
|
|
2796
|
+
files.push({
|
|
2797
|
+
type: parts[0],
|
|
2798
|
+
name: parts[1],
|
|
2799
|
+
module: parts[2],
|
|
2800
|
+
path: parts[3],
|
|
2801
|
+
hash: parts[4] || null, // Hash may not exist in old manifests
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
return files;
|
|
2807
|
+
} catch (error) {
|
|
2808
|
+
await prompts.log.warn('Could not read files-manifest.csv: ' + error.message);
|
|
2809
|
+
return [];
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
/**
|
|
2814
|
+
* Detect custom and modified files
|
|
2815
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
2816
|
+
* @param {Array} existingFilesManifest - Previous files from files-manifest.csv
|
|
2817
|
+
* @returns {Object} Object with customFiles and modifiedFiles arrays
|
|
2818
|
+
*/
|
|
2819
|
+
async detectCustomFiles(bmadDir, existingFilesManifest) {
|
|
2820
|
+
const customFiles = [];
|
|
2821
|
+
const modifiedFiles = [];
|
|
2822
|
+
|
|
2823
|
+
// Memory is always in _bmad/_memory
|
|
2824
|
+
const bmadMemoryPath = '_memory';
|
|
2825
|
+
|
|
2826
|
+
// Check if the manifest has hashes - if not, we can't detect modifications
|
|
2827
|
+
let manifestHasHashes = false;
|
|
2828
|
+
if (existingFilesManifest && existingFilesManifest.length > 0) {
|
|
2829
|
+
manifestHasHashes = existingFilesManifest.some((f) => f.hash);
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// Build map of previously installed files from files-manifest.csv with their hashes
|
|
2833
|
+
const installedFilesMap = new Map();
|
|
2834
|
+
for (const fileEntry of existingFilesManifest) {
|
|
2835
|
+
if (fileEntry.path) {
|
|
2836
|
+
const absolutePath = path.join(bmadDir, fileEntry.path);
|
|
2837
|
+
installedFilesMap.set(path.normalize(absolutePath), {
|
|
2838
|
+
hash: fileEntry.hash,
|
|
2839
|
+
relativePath: fileEntry.path,
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// Recursively scan bmadDir for all files
|
|
2845
|
+
const scanDirectory = async (dir) => {
|
|
2846
|
+
try {
|
|
2847
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2848
|
+
for (const entry of entries) {
|
|
2849
|
+
const fullPath = path.join(dir, entry.name);
|
|
2850
|
+
|
|
2851
|
+
if (entry.isDirectory()) {
|
|
2852
|
+
// Skip certain directories
|
|
2853
|
+
if (entry.name === 'node_modules' || entry.name === '.git') {
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
await scanDirectory(fullPath);
|
|
2857
|
+
} else if (entry.isFile()) {
|
|
2858
|
+
const normalizedPath = path.normalize(fullPath);
|
|
2859
|
+
const fileInfo = installedFilesMap.get(normalizedPath);
|
|
2860
|
+
|
|
2861
|
+
// Skip certain system files that are auto-generated
|
|
2862
|
+
const relativePath = path.relative(bmadDir, fullPath);
|
|
2863
|
+
const fileName = path.basename(fullPath);
|
|
2864
|
+
|
|
2865
|
+
// Skip _config directory EXCEPT for modified agent customizations
|
|
2866
|
+
if (relativePath.startsWith('_config/') || relativePath.startsWith('_config\\')) {
|
|
2867
|
+
// Special handling for .customize.yaml files - only preserve if modified
|
|
2868
|
+
if (relativePath.includes('/agents/') && fileName.endsWith('.customize.yaml')) {
|
|
2869
|
+
// Check if the customization file has been modified from manifest
|
|
2870
|
+
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
|
|
2871
|
+
if (await fs.pathExists(manifestPath)) {
|
|
2872
|
+
const crypto = require('node:crypto');
|
|
2873
|
+
const currentContent = await fs.readFile(fullPath, 'utf8');
|
|
2874
|
+
const currentHash = crypto.createHash('sha256').update(currentContent).digest('hex');
|
|
2875
|
+
|
|
2876
|
+
const yaml = require('yaml');
|
|
2877
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf8');
|
|
2878
|
+
const manifestData = yaml.parse(manifestContent);
|
|
2879
|
+
const originalHash = manifestData.agentCustomizations?.[relativePath];
|
|
2880
|
+
|
|
2881
|
+
// Only add to customFiles if hash differs (user modified)
|
|
2882
|
+
if (originalHash && currentHash !== originalHash) {
|
|
2883
|
+
customFiles.push(fullPath);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
if (relativePath.startsWith(bmadMemoryPath + '/') && path.dirname(relativePath).includes('-sidecar')) {
|
|
2891
|
+
continue;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// Skip config.yaml files - these are regenerated on each install/update
|
|
2895
|
+
if (fileName === 'config.yaml') {
|
|
2896
|
+
continue;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
if (!fileInfo) {
|
|
2900
|
+
// File not in manifest = custom file
|
|
2901
|
+
// EXCEPT: Agent .md files in module folders are generated files, not custom
|
|
2902
|
+
// Only treat .md files under _config/agents/ as custom
|
|
2903
|
+
if (!(fileName.endsWith('.md') && relativePath.includes('/agents/') && !relativePath.startsWith('_config/'))) {
|
|
2904
|
+
customFiles.push(fullPath);
|
|
2905
|
+
}
|
|
2906
|
+
} else if (manifestHasHashes && fileInfo.hash) {
|
|
2907
|
+
// File in manifest with hash - check if it was modified
|
|
2908
|
+
const currentHash = await this.manifest.calculateFileHash(fullPath);
|
|
2909
|
+
if (currentHash && currentHash !== fileInfo.hash) {
|
|
2910
|
+
// Hash changed = file was modified
|
|
2911
|
+
modifiedFiles.push({
|
|
2912
|
+
path: fullPath,
|
|
2913
|
+
relativePath: fileInfo.relativePath,
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
} catch {
|
|
2920
|
+
// Ignore errors scanning directories
|
|
2921
|
+
}
|
|
2922
|
+
};
|
|
2923
|
+
|
|
2924
|
+
await scanDirectory(bmadDir);
|
|
2925
|
+
return { customFiles, modifiedFiles };
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/**
|
|
2929
|
+
* Handle missing custom module sources interactively
|
|
2930
|
+
* @param {Map} customModuleSources - Map of custom module ID to info
|
|
2931
|
+
* @param {string} bmadDir - BMAD directory
|
|
2932
|
+
* @param {string} projectRoot - Project root directory
|
|
2933
|
+
* @param {string} operation - Current operation ('update', 'compile', etc.)
|
|
2934
|
+
* @param {Array} installedModules - Array of installed module IDs (will be modified)
|
|
2935
|
+
* @param {boolean} [skipPrompts=false] - Skip interactive prompts and keep all modules with missing sources
|
|
2936
|
+
* @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
|
|
2937
|
+
*/
|
|
2938
|
+
async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules, skipPrompts = false) {
|
|
2939
|
+
const validCustomModules = [];
|
|
2940
|
+
const keptModulesWithoutSources = []; // Track modules kept without sources
|
|
2941
|
+
const customModulesWithMissingSources = [];
|
|
2942
|
+
|
|
2943
|
+
// Check which sources exist
|
|
2944
|
+
for (const [moduleId, customInfo] of customModuleSources) {
|
|
2945
|
+
if (await fs.pathExists(customInfo.sourcePath)) {
|
|
2946
|
+
validCustomModules.push({
|
|
2947
|
+
id: moduleId,
|
|
2948
|
+
name: customInfo.name,
|
|
2949
|
+
path: customInfo.sourcePath,
|
|
2950
|
+
info: customInfo,
|
|
2951
|
+
});
|
|
2952
|
+
} else {
|
|
2953
|
+
// For cached modules that are missing, we just skip them without prompting
|
|
2954
|
+
if (customInfo.cached) {
|
|
2955
|
+
// Skip cached modules without prompting
|
|
2956
|
+
keptModulesWithoutSources.push({
|
|
2957
|
+
id: moduleId,
|
|
2958
|
+
name: customInfo.name,
|
|
2959
|
+
cached: true,
|
|
2960
|
+
});
|
|
2961
|
+
} else {
|
|
2962
|
+
customModulesWithMissingSources.push({
|
|
2963
|
+
id: moduleId,
|
|
2964
|
+
name: customInfo.name,
|
|
2965
|
+
sourcePath: customInfo.sourcePath,
|
|
2966
|
+
relativePath: customInfo.relativePath,
|
|
2967
|
+
info: customInfo,
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
// If no missing sources, return immediately
|
|
2974
|
+
if (customModulesWithMissingSources.length === 0) {
|
|
2975
|
+
return {
|
|
2976
|
+
validCustomModules,
|
|
2977
|
+
keptModulesWithoutSources: [],
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
// Non-interactive mode: keep all modules with missing sources
|
|
2982
|
+
if (skipPrompts) {
|
|
2983
|
+
for (const missing of customModulesWithMissingSources) {
|
|
2984
|
+
keptModulesWithoutSources.push(missing.id);
|
|
2985
|
+
}
|
|
2986
|
+
return { validCustomModules, keptModulesWithoutSources };
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`);
|
|
2990
|
+
|
|
2991
|
+
let keptCount = 0;
|
|
2992
|
+
let updatedCount = 0;
|
|
2993
|
+
let removedCount = 0;
|
|
2994
|
+
|
|
2995
|
+
for (const missing of customModulesWithMissingSources) {
|
|
2996
|
+
await prompts.log.message(
|
|
2997
|
+
`${missing.name} (${missing.id})\n Original source: ${missing.relativePath}\n Full path: ${missing.sourcePath}`,
|
|
2998
|
+
);
|
|
2999
|
+
|
|
3000
|
+
const choices = [
|
|
3001
|
+
{
|
|
3002
|
+
name: 'Keep installed (will not be processed)',
|
|
3003
|
+
value: 'keep',
|
|
3004
|
+
hint: 'Keep',
|
|
3005
|
+
},
|
|
3006
|
+
{
|
|
3007
|
+
name: 'Specify new source location',
|
|
3008
|
+
value: 'update',
|
|
3009
|
+
hint: 'Update',
|
|
3010
|
+
},
|
|
3011
|
+
];
|
|
3012
|
+
|
|
3013
|
+
// Only add remove option if not just compiling agents
|
|
3014
|
+
if (operation !== 'compile-agents') {
|
|
3015
|
+
choices.push({
|
|
3016
|
+
name: '⚠️ REMOVE module completely (destructive!)',
|
|
3017
|
+
value: 'remove',
|
|
3018
|
+
hint: 'Remove',
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
const action = await prompts.select({
|
|
3023
|
+
message: `How would you like to handle "${missing.name}"?`,
|
|
3024
|
+
choices,
|
|
3025
|
+
});
|
|
3026
|
+
|
|
3027
|
+
switch (action) {
|
|
3028
|
+
case 'update': {
|
|
3029
|
+
// Use sync validation because @clack/prompts doesn't support async validate
|
|
3030
|
+
const newSourcePath = await prompts.text({
|
|
3031
|
+
message: 'Enter the new path to the custom module:',
|
|
3032
|
+
default: missing.sourcePath,
|
|
3033
|
+
validate: (input) => {
|
|
3034
|
+
if (!input || input.trim() === '') {
|
|
3035
|
+
return 'Please enter a path';
|
|
3036
|
+
}
|
|
3037
|
+
const expandedPath = path.resolve(input.trim());
|
|
3038
|
+
if (!fs.pathExistsSync(expandedPath)) {
|
|
3039
|
+
return 'Path does not exist';
|
|
3040
|
+
}
|
|
3041
|
+
// Check if it looks like a valid module
|
|
3042
|
+
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
3043
|
+
const agentsPath = path.join(expandedPath, 'agents');
|
|
3044
|
+
const workflowsPath = path.join(expandedPath, 'workflows');
|
|
3045
|
+
|
|
3046
|
+
if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
|
|
3047
|
+
return 'Path does not appear to contain a valid custom module';
|
|
3048
|
+
}
|
|
3049
|
+
return; // clack expects undefined for valid input
|
|
3050
|
+
},
|
|
3051
|
+
});
|
|
3052
|
+
|
|
3053
|
+
// Defensive: handleCancel should have exited, but guard against symbol propagation
|
|
3054
|
+
if (typeof newSourcePath !== 'string') {
|
|
3055
|
+
keptCount++;
|
|
3056
|
+
keptModulesWithoutSources.push(missing.id);
|
|
3057
|
+
continue;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
// Update the source in manifest
|
|
3061
|
+
const resolvedPath = path.resolve(newSourcePath.trim());
|
|
3062
|
+
missing.info.sourcePath = resolvedPath;
|
|
3063
|
+
// Remove relativePath - we only store absolute sourcePath now
|
|
3064
|
+
delete missing.info.relativePath;
|
|
3065
|
+
await this.manifest.addCustomModule(bmadDir, missing.info);
|
|
3066
|
+
|
|
3067
|
+
validCustomModules.push({
|
|
3068
|
+
id: missing.id,
|
|
3069
|
+
name: missing.name,
|
|
3070
|
+
path: resolvedPath,
|
|
3071
|
+
info: missing.info,
|
|
3072
|
+
});
|
|
3073
|
+
|
|
3074
|
+
updatedCount++;
|
|
3075
|
+
await prompts.log.success('Updated source location');
|
|
3076
|
+
|
|
3077
|
+
break;
|
|
3078
|
+
}
|
|
3079
|
+
case 'remove': {
|
|
3080
|
+
// Extra confirmation for destructive remove
|
|
3081
|
+
await prompts.log.error(
|
|
3082
|
+
`WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!\n Module location: ${path.join(bmadDir, missing.id)}`,
|
|
3083
|
+
);
|
|
3084
|
+
|
|
3085
|
+
const confirmDelete = await prompts.confirm({
|
|
3086
|
+
message: 'Are you absolutely sure you want to delete this module?',
|
|
3087
|
+
default: false,
|
|
3088
|
+
});
|
|
3089
|
+
|
|
3090
|
+
if (confirmDelete) {
|
|
3091
|
+
const typedConfirm = await prompts.text({
|
|
3092
|
+
message: 'Type "DELETE" to confirm permanent deletion:',
|
|
3093
|
+
validate: (input) => {
|
|
3094
|
+
if (input !== 'DELETE') {
|
|
3095
|
+
return 'You must type "DELETE" exactly to proceed';
|
|
3096
|
+
}
|
|
3097
|
+
return; // clack expects undefined for valid input
|
|
3098
|
+
},
|
|
3099
|
+
});
|
|
3100
|
+
|
|
3101
|
+
if (typedConfirm === 'DELETE') {
|
|
3102
|
+
// Remove the module from filesystem and manifest
|
|
3103
|
+
const modulePath = path.join(bmadDir, missing.id);
|
|
3104
|
+
if (await fs.pathExists(modulePath)) {
|
|
3105
|
+
const fsExtra = require('fs-extra');
|
|
3106
|
+
await fsExtra.remove(modulePath);
|
|
3107
|
+
await prompts.log.warn(`Deleted module directory: ${path.relative(projectRoot, modulePath)}`);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
await this.manifest.removeModule(bmadDir, missing.id);
|
|
3111
|
+
await this.manifest.removeCustomModule(bmadDir, missing.id);
|
|
3112
|
+
await prompts.log.warn('Removed from manifest');
|
|
3113
|
+
|
|
3114
|
+
// Also remove from installedModules list
|
|
3115
|
+
if (installedModules && installedModules.includes(missing.id)) {
|
|
3116
|
+
const index = installedModules.indexOf(missing.id);
|
|
3117
|
+
if (index !== -1) {
|
|
3118
|
+
installedModules.splice(index, 1);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
removedCount++;
|
|
3123
|
+
await prompts.log.error(`"${missing.name}" has been permanently removed`);
|
|
3124
|
+
} else {
|
|
3125
|
+
await prompts.log.message('Removal cancelled - module will be kept');
|
|
3126
|
+
keptCount++;
|
|
3127
|
+
}
|
|
3128
|
+
} else {
|
|
3129
|
+
await prompts.log.message('Removal cancelled - module will be kept');
|
|
3130
|
+
keptCount++;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
break;
|
|
3134
|
+
}
|
|
3135
|
+
case 'keep': {
|
|
3136
|
+
keptCount++;
|
|
3137
|
+
keptModulesWithoutSources.push(missing.id);
|
|
3138
|
+
await prompts.log.message('Module will be kept as-is');
|
|
3139
|
+
|
|
3140
|
+
break;
|
|
3141
|
+
}
|
|
3142
|
+
// No default
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
// Show summary
|
|
3147
|
+
if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
|
|
3148
|
+
let summary = 'Summary for custom modules with missing sources:';
|
|
3149
|
+
if (keptCount > 0) summary += `\n • ${keptCount} module(s) kept as-is`;
|
|
3150
|
+
if (updatedCount > 0) summary += `\n • ${updatedCount} module(s) updated with new sources`;
|
|
3151
|
+
if (removedCount > 0) summary += `\n • ${removedCount} module(s) permanently deleted`;
|
|
3152
|
+
await prompts.log.message(summary);
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
return {
|
|
3156
|
+
validCustomModules,
|
|
3157
|
+
keptModulesWithoutSources,
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
module.exports = { Installer };
|