bmad-method 6.2.3-next.22 → 6.2.3-next.24
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/README.md +7 -8
- package/package.json +1 -1
- package/removals.txt +17 -0
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/data/prd-purpose.md +197 -0
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01-discovery.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01b-legacy-conversion.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-02-review.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-03-edit.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +1 -3
- package/tools/installer/cli-utils.js +18 -9
- package/tools/installer/core/installer.js +143 -33
- package/tools/installer/core/manifest-generator.js +3 -11
- package/tools/installer/core/manifest.js +61 -35
- package/tools/installer/ide/_config-driven.js +136 -32
- package/tools/installer/ide/shared/skill-manifest.js +1 -16
- package/tools/installer/install-messages.yaml +19 -26
- package/tools/installer/ui.js +54 -11
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/bmad-method)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
|
+
[](https://www.python.org)
|
|
7
|
+
[](https://docs.astral.sh/uv/)
|
|
6
8
|
[](https://discord.gg/gk8jAdXWmj)
|
|
7
9
|
|
|
8
10
|
**Build More Architect Dreams** — An AI-driven agile development module for the BMad Method Module Ecosystem, the best and most comprehensive Agile AI Driven Development framework that has true scale-adaptive intelligence that adjusts from bug fixes to enterprise systems.
|
|
@@ -34,7 +36,7 @@ Traditional AI tools do the thinking for you, producing average results. BMad ag
|
|
|
34
36
|
|
|
35
37
|
## Quick Start
|
|
36
38
|
|
|
37
|
-
**Prerequisites**: [Node.js](https://nodejs.org) v20+
|
|
39
|
+
**Prerequisites**: [Node.js](https://nodejs.org) v20+ · [Python](https://www.python.org) 3.10+ · [uv](https://docs.astral.sh/uv/)
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
42
|
npx bmad-method install
|
|
@@ -79,18 +81,15 @@ BMad Method extends with official modules for specialized domains. Available dur
|
|
|
79
81
|
## Community
|
|
80
82
|
|
|
81
83
|
- [Discord](https://discord.gg/gk8jAdXWmj) — Get help, share ideas, collaborate
|
|
82
|
-
- [
|
|
84
|
+
- [YouTube](https://youtube.com/@BMadCode) — Tutorials, master class, and more
|
|
85
|
+
- [X / Twitter](https://x.com/BMadCode)
|
|
86
|
+
- [Website](https://bmadcode.com)
|
|
83
87
|
- [GitHub Issues](https://github.com/bmad-code-org/BMAD-METHOD/issues) — Bug reports and feature requests
|
|
84
88
|
- [Discussions](https://github.com/bmad-code-org/BMAD-METHOD/discussions) — Community conversations
|
|
85
89
|
|
|
86
90
|
## Support BMad
|
|
87
91
|
|
|
88
|
-
BMad is free for everyone
|
|
89
|
-
|
|
90
|
-
- ⭐ Please click the star project icon near the top right of this page
|
|
91
|
-
- ☕ [Buy Me a Coffee](https://buymeacoffee.com/bmad) — Fuel the development
|
|
92
|
-
- 🏢 Corporate sponsorship — DM on Discord
|
|
93
|
-
- 🎤 Speaking & Media — Available for conferences, podcasts, interviews (BM on Discord)
|
|
92
|
+
BMad is free for everyone and always will be. Star this repo, [buy me a coffee](https://buymeacoffee.com/bmad), or email <contact@bmadcode.com> for corporate sponsorship.
|
|
94
93
|
|
|
95
94
|
## Contributing
|
|
96
95
|
|
package/package.json
CHANGED
package/removals.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# BMad Method - Skill Removal List
|
|
2
|
+
# Entries listed here will be removed from IDE skill directories during install/update.
|
|
3
|
+
# One entry per line. Lines starting with # are comments.
|
|
4
|
+
# Each entry is a skill directory name (canonicalId) that was removed or renamed.
|
|
5
|
+
|
|
6
|
+
# Removed agents (v6.2.0 - v6.2.2)
|
|
7
|
+
bmad-agent-sm
|
|
8
|
+
bmad-agent-qa
|
|
9
|
+
bmad-agent-quick-flow-solo-dev
|
|
10
|
+
|
|
11
|
+
# Removed skills (v6.2.0 - v6.2.2)
|
|
12
|
+
bmad-create-product-brief
|
|
13
|
+
bmad-product-brief-preview
|
|
14
|
+
bmad-quick-spec
|
|
15
|
+
bmad-quick-flow
|
|
16
|
+
bmad-quick-dev-new-preview
|
|
17
|
+
bmad-init
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# BMAD PRD Purpose
|
|
2
|
+
|
|
3
|
+
**The PRD is the top of the required funnel that feeds all subsequent product development work in rhw BMad Method.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What is a BMAD PRD?
|
|
8
|
+
|
|
9
|
+
A dual-audience document serving:
|
|
10
|
+
1. **Human Product Managers and builders** - Vision, strategy, stakeholder communication
|
|
11
|
+
2. **LLM Downstream Consumption** - UX Design → Architecture → Epics → Development AI Agents
|
|
12
|
+
|
|
13
|
+
Each successive document becomes more AI-tailored and granular.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Philosophy: Information Density
|
|
18
|
+
|
|
19
|
+
**High Signal-to-Noise Ratio**
|
|
20
|
+
|
|
21
|
+
Every sentence must carry information weight. LLMs consume precise, dense content efficiently.
|
|
22
|
+
|
|
23
|
+
**Anti-Patterns (Eliminate These):**
|
|
24
|
+
- ❌ "The system will allow users to..." → ✅ "Users can..."
|
|
25
|
+
- ❌ "It is important to note that..." → ✅ State the fact directly
|
|
26
|
+
- ❌ "In order to..." → ✅ "To..."
|
|
27
|
+
- ❌ Conversational filler and padding → ✅ Direct, concise statements
|
|
28
|
+
|
|
29
|
+
**Goal:** Maximum information per word. Zero fluff.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## The Traceability Chain
|
|
34
|
+
|
|
35
|
+
**PRD starts the chain:**
|
|
36
|
+
```
|
|
37
|
+
Vision → Success Criteria → User Journeys → Functional Requirements → (future: User Stories)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**In the PRD, establish:**
|
|
41
|
+
- Vision → Success Criteria alignment
|
|
42
|
+
- Success Criteria → User Journey coverage
|
|
43
|
+
- User Journey → Functional Requirement mapping
|
|
44
|
+
- All requirements traceable to user needs
|
|
45
|
+
|
|
46
|
+
**Why:** Each downstream artifact (UX, Architecture, Epics, Stories) must trace back to documented user needs and business objectives. This chain ensures we build the right thing.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## What Makes Great Functional Requirements?
|
|
51
|
+
|
|
52
|
+
### FRs are Capabilities, Not Implementation
|
|
53
|
+
|
|
54
|
+
**Good FR:** "Users can reset their password via email link"
|
|
55
|
+
**Bad FR:** "System sends JWT via email and validates with database" (implementation leakage)
|
|
56
|
+
|
|
57
|
+
**Good FR:** "Dashboard loads in under 2 seconds for 95th percentile"
|
|
58
|
+
**Bad FR:** "Fast loading time" (subjective, unmeasurable)
|
|
59
|
+
|
|
60
|
+
### SMART Quality Criteria
|
|
61
|
+
|
|
62
|
+
**Specific:** Clear, precisely defined capability
|
|
63
|
+
**Measurable:** Quantifiable with test criteria
|
|
64
|
+
**Attainable:** Realistic within constraints
|
|
65
|
+
**Relevant:** Aligns with business objectives
|
|
66
|
+
**Traceable:** Links to source (executive summary or user journey)
|
|
67
|
+
|
|
68
|
+
### FR Anti-Patterns
|
|
69
|
+
|
|
70
|
+
**Subjective Adjectives:**
|
|
71
|
+
- ❌ "easy to use", "intuitive", "user-friendly", "fast", "responsive"
|
|
72
|
+
- ✅ Use metrics: "completes task in under 3 clicks", "loads in under 2 seconds"
|
|
73
|
+
|
|
74
|
+
**Implementation Leakage:**
|
|
75
|
+
- ❌ Technology names, specific libraries, implementation details
|
|
76
|
+
- ✅ Focus on capability and measurable outcomes
|
|
77
|
+
|
|
78
|
+
**Vague Quantifiers:**
|
|
79
|
+
- ❌ "multiple users", "several options", "various formats"
|
|
80
|
+
- ✅ "up to 100 concurrent users", "3-5 options", "PDF, DOCX, TXT formats"
|
|
81
|
+
|
|
82
|
+
**Missing Test Criteria:**
|
|
83
|
+
- ❌ "The system shall provide notifications"
|
|
84
|
+
- ✅ "The system shall send email notifications within 30 seconds of trigger event"
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## What Makes Great Non-Functional Requirements?
|
|
89
|
+
|
|
90
|
+
### NFRs Must Be Measurable
|
|
91
|
+
|
|
92
|
+
**Template:**
|
|
93
|
+
```
|
|
94
|
+
"The system shall [metric] [condition] [measurement method]"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Examples:**
|
|
98
|
+
- ✅ "The system shall respond to API requests in under 200ms for 95th percentile as measured by APM monitoring"
|
|
99
|
+
- ✅ "The system shall maintain 99.9% uptime during business hours as measured by cloud provider SLA"
|
|
100
|
+
- ✅ "The system shall support 10,000 concurrent users as measured by load testing"
|
|
101
|
+
|
|
102
|
+
### NFR Anti-Patterns
|
|
103
|
+
|
|
104
|
+
**Unmeasurable Claims:**
|
|
105
|
+
- ❌ "The system shall be scalable" → ✅ "The system shall handle 10x load growth through horizontal scaling"
|
|
106
|
+
- ❌ "High availability required" → ✅ "99.9% uptime as measured by cloud provider SLA"
|
|
107
|
+
|
|
108
|
+
**Missing Context:**
|
|
109
|
+
- ❌ "Response time under 1 second" → ✅ "API response time under 1 second for 95th percentile under normal load"
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Domain-Specific Requirements
|
|
114
|
+
|
|
115
|
+
**Auto-Detect and Enforce Based on Project Context**
|
|
116
|
+
|
|
117
|
+
Certain industries have mandatory requirements that must be present:
|
|
118
|
+
|
|
119
|
+
- **Healthcare:** HIPAA Privacy & Security Rules, PHI encryption, audit logging, MFA
|
|
120
|
+
- **Fintech:** PCI-DSS Level 1, AML/KYC compliance, SOX controls, financial audit trails
|
|
121
|
+
- **GovTech:** NIST framework, Section 508 accessibility (WCAG 2.1 AA), FedRAMP, data residency
|
|
122
|
+
- **E-Commerce:** PCI-DSS for payments, inventory accuracy, tax calculation by jurisdiction
|
|
123
|
+
|
|
124
|
+
**Why:** Missing these requirements in the PRD means they'll be missed in architecture and implementation, creating expensive rework. During PRD creation there is a step to cover this - during validation we want to make sure it was covered. For this purpose steps will utilize a domain-complexity.csv and project-types.csv.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Document Structure (Markdown, Human-Readable)
|
|
129
|
+
|
|
130
|
+
### Required Sections
|
|
131
|
+
1. **Executive Summary** - Vision, differentiator, target users
|
|
132
|
+
2. **Success Criteria** - Measurable outcomes (SMART)
|
|
133
|
+
3. **Product Scope** - MVP, Growth, Vision phases
|
|
134
|
+
4. **User Journeys** - Comprehensive coverage
|
|
135
|
+
5. **Domain Requirements** - Industry-specific compliance (if applicable)
|
|
136
|
+
6. **Innovation Analysis** - Competitive differentiation (if applicable)
|
|
137
|
+
7. **Project-Type Requirements** - Platform-specific needs
|
|
138
|
+
8. **Functional Requirements** - Capability contract (FRs)
|
|
139
|
+
9. **Non-Functional Requirements** - Quality attributes (NFRs)
|
|
140
|
+
|
|
141
|
+
### Formatting for Dual Consumption
|
|
142
|
+
|
|
143
|
+
**For Humans:**
|
|
144
|
+
- Clear, professional language
|
|
145
|
+
- Logical flow from vision to requirements
|
|
146
|
+
- Easy for stakeholders to review and approve
|
|
147
|
+
|
|
148
|
+
**For LLMs:**
|
|
149
|
+
- ## Level 2 headers for all main sections (enables extraction)
|
|
150
|
+
- Consistent structure and patterns
|
|
151
|
+
- Precise, testable language
|
|
152
|
+
- High information density
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Downstream Impact
|
|
157
|
+
|
|
158
|
+
**How the PRD Feeds Next Artifacts:**
|
|
159
|
+
|
|
160
|
+
**UX Design:**
|
|
161
|
+
- User journeys → interaction flows
|
|
162
|
+
- FRs → design requirements
|
|
163
|
+
- Success criteria → UX metrics
|
|
164
|
+
|
|
165
|
+
**Architecture:**
|
|
166
|
+
- FRs → system capabilities
|
|
167
|
+
- NFRs → architecture decisions
|
|
168
|
+
- Domain requirements → compliance architecture
|
|
169
|
+
- Project-type requirements → platform choices
|
|
170
|
+
|
|
171
|
+
**Epics & Stories (created after architecture):**
|
|
172
|
+
- FRs → user stories (1 FR could map to 1-3 stories potentially)
|
|
173
|
+
- Acceptance criteria → story acceptance tests
|
|
174
|
+
- Priority → sprint sequencing
|
|
175
|
+
- Traceability → stories map back to vision
|
|
176
|
+
|
|
177
|
+
**Development AI Agents:**
|
|
178
|
+
- Precise requirements → implementation clarity
|
|
179
|
+
- Test criteria → automated test generation
|
|
180
|
+
- Domain requirements → compliance enforcement
|
|
181
|
+
- Measurable NFRs → performance targets
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Summary: What Makes a Great BMAD PRD?
|
|
186
|
+
|
|
187
|
+
✅ **High Information Density** - Every sentence carries weight, zero fluff
|
|
188
|
+
✅ **Measurable Requirements** - All FRs and NFRs are testable with specific criteria
|
|
189
|
+
✅ **Clear Traceability** - Each requirement links to user need and business objective
|
|
190
|
+
✅ **Domain Awareness** - Industry-specific requirements auto-detected and included
|
|
191
|
+
✅ **Zero Anti-Patterns** - No subjective adjectives, implementation leakage, or vague quantifiers
|
|
192
|
+
✅ **Dual Audience Optimized** - Human-readable AND LLM-consumable
|
|
193
|
+
✅ **Markdown Format** - Professional, clean, accessible to all stakeholders
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
**Remember:** The PRD is the foundation. Quality here ripples through every subsequent phase. A dense, precise, well-traced PRD makes UX design, architecture, epic breakdown, and AI development dramatically more effective.
|
package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01b-legacy-conversion.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
# File references (ONLY variables used in this step)
|
|
3
3
|
prdFile: '{prd_file_path}'
|
|
4
|
-
prdPurpose: '
|
|
4
|
+
prdPurpose: '../data/prd-purpose.md'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Step E-1B: Legacy PRD Conversion Assessment
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# File references (ONLY variables used in this step)
|
|
3
3
|
prdFile: '{prd_file_path}'
|
|
4
4
|
validationReport: '{validation_report_path}' # If provided
|
|
5
|
-
prdPurpose: '
|
|
5
|
+
prdPurpose: '../data/prd-purpose.md'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Step E-2: Deep Review & Analysis
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
# File references (ONLY variables used in this step)
|
|
3
3
|
prdFile: '{prd_file_path}'
|
|
4
|
-
validationWorkflow: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-01-discovery.md'
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# Step E-4: Complete & Validate
|
|
@@ -117,8 +116,7 @@ Display:
|
|
|
117
116
|
- Display: "This will run all 13 validation checks on the updated PRD."
|
|
118
117
|
- Display: "Preparing to validate: {prd_file_path}"
|
|
119
118
|
- Display: "**Proceeding to validation...**"
|
|
120
|
-
-
|
|
121
|
-
- Note: This hands off to the validation workflow which will run its complete 13-step process
|
|
119
|
+
- Invoke the `bmad-validate-prd` skill to run the complete validation workflow
|
|
122
120
|
|
|
123
121
|
- **IF E (Edit More):**
|
|
124
122
|
- Display: "**Additional Edits**"
|
|
@@ -19,24 +19,33 @@ const CLIUtils = {
|
|
|
19
19
|
* Display BMAD logo and version using @clack intro + box
|
|
20
20
|
*/
|
|
21
21
|
async displayLogo() {
|
|
22
|
-
const version = this.getVersion();
|
|
23
22
|
const color = await prompts.getColor();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const termWidth = process.stdout.columns || 80;
|
|
24
|
+
|
|
25
|
+
// Full "BMad Method" logo for wide terminals, "BMad" only for narrow
|
|
26
|
+
const logoWide = [
|
|
27
|
+
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ™',
|
|
28
|
+
'██╔══██╗████╗ ████║██╔══██╗██╔══██╗ ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗',
|
|
29
|
+
'██████╔╝██╔████╔██║███████║██║ ██║ ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║',
|
|
30
|
+
'██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║ ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║',
|
|
31
|
+
'██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝',
|
|
32
|
+
'╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const logoNarrow = [
|
|
27
36
|
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
|
|
28
37
|
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
|
|
29
38
|
' ██████╔╝██╔████╔██║███████║██║ ██║',
|
|
30
39
|
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
|
|
31
40
|
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
|
|
32
41
|
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
|
|
33
|
-
]
|
|
34
|
-
.map((line) => color.yellow(line))
|
|
35
|
-
.join('\n');
|
|
42
|
+
];
|
|
36
43
|
|
|
37
|
-
const
|
|
44
|
+
const logoLines = termWidth >= 95 ? logoWide : logoNarrow;
|
|
45
|
+
const logo = logoLines.map((line) => color.blue(line)).join('\n');
|
|
46
|
+
const tagline = color.white(' Build More, Architect Dreams\n © BMad Code');
|
|
38
47
|
|
|
39
|
-
await prompts.box(`${logo}\n${tagline}`,
|
|
48
|
+
await prompts.box(`${logo}\n${tagline}`, '', {
|
|
40
49
|
contentAlign: 'center',
|
|
41
50
|
rounded: true,
|
|
42
51
|
formatBorder: color.blue,
|
|
@@ -26,6 +26,44 @@ class Installer {
|
|
|
26
26
|
this.bmadFolderName = BMAD_FOLDER_NAME;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Read the module version from .claude-plugin/marketplace.json
|
|
31
|
+
* Walks up from sourcePath looking for .claude-plugin/marketplace.json
|
|
32
|
+
* @param {string} sourcePath - Module source directory
|
|
33
|
+
* @returns {string} Version string or empty string
|
|
34
|
+
*/
|
|
35
|
+
async _getMarketplaceVersion(sourcePath) {
|
|
36
|
+
let dir = sourcePath;
|
|
37
|
+
for (let i = 0; i < 5; i++) {
|
|
38
|
+
const marketplacePath = path.join(dir, '.claude-plugin', 'marketplace.json');
|
|
39
|
+
if (await fs.pathExists(marketplacePath)) {
|
|
40
|
+
try {
|
|
41
|
+
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
|
42
|
+
return this._extractMarketplaceVersion(data);
|
|
43
|
+
} catch {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const parent = path.dirname(dir);
|
|
48
|
+
if (parent === dir) break;
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract the highest version from marketplace.json plugins array
|
|
56
|
+
*/
|
|
57
|
+
_extractMarketplaceVersion(data) {
|
|
58
|
+
const plugins = data?.plugins;
|
|
59
|
+
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
|
60
|
+
let best = '';
|
|
61
|
+
for (const p of plugins) {
|
|
62
|
+
if (p.version && (!best || p.version > best)) best = p.version;
|
|
63
|
+
}
|
|
64
|
+
return best;
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
/**
|
|
30
68
|
* Main installation method
|
|
31
69
|
* @param {Object} config - Installation configuration
|
|
@@ -52,9 +90,36 @@ class Installer {
|
|
|
52
90
|
|
|
53
91
|
await this._validateIdeSelection(config);
|
|
54
92
|
|
|
93
|
+
// Capture pre-install module versions for from→to display
|
|
94
|
+
const preInstallVersions = new Map();
|
|
95
|
+
if (existingInstall.installed) {
|
|
96
|
+
const existingModules = await this.manifest.getAllModuleVersions(paths.bmadDir);
|
|
97
|
+
for (const mod of existingModules) {
|
|
98
|
+
if (mod.name && mod.version) {
|
|
99
|
+
preInstallVersions.set(mod.name, mod.version);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
55
104
|
// Results collector for consolidated summary
|
|
56
105
|
const results = [];
|
|
57
|
-
const addResult = (step, status, detail = '') => results.push({ step, status, detail });
|
|
106
|
+
const addResult = (step, status, detail = '', meta = {}) => results.push({ step, status, detail, ...meta });
|
|
107
|
+
|
|
108
|
+
// Capture previously installed skill IDs before they get overwritten
|
|
109
|
+
const previousSkillIds = new Set();
|
|
110
|
+
const prevCsvPath = path.join(paths.bmadDir, '_config', 'skill-manifest.csv');
|
|
111
|
+
if (await fs.pathExists(prevCsvPath)) {
|
|
112
|
+
try {
|
|
113
|
+
const csvParse = require('csv-parse/sync');
|
|
114
|
+
const content = await fs.readFile(prevCsvPath, 'utf8');
|
|
115
|
+
const records = csvParse.parse(content, { columns: true, skip_empty_lines: true });
|
|
116
|
+
for (const r of records) {
|
|
117
|
+
if (r.canonicalId) previousSkillIds.add(r.canonicalId);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
58
123
|
|
|
59
124
|
await this._cacheCustomModules(paths, addResult);
|
|
60
125
|
|
|
@@ -65,7 +130,11 @@ class Installer {
|
|
|
65
130
|
|
|
66
131
|
await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules);
|
|
67
132
|
|
|
68
|
-
await this._setupIdes(config, allModules, paths, addResult);
|
|
133
|
+
await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
|
|
134
|
+
|
|
135
|
+
// Skills are now in IDE directories — remove redundant copies from _bmad/.
|
|
136
|
+
// Also cleans up skill dirs left by older installer versions.
|
|
137
|
+
await this._cleanupSkillDirs(paths.bmadDir);
|
|
69
138
|
|
|
70
139
|
const restoreResult = await this._restoreUserFiles(paths, updateState);
|
|
71
140
|
|
|
@@ -76,6 +145,7 @@ class Installer {
|
|
|
76
145
|
ides: config.ides,
|
|
77
146
|
customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
|
|
78
147
|
modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
|
|
148
|
+
preInstallVersions,
|
|
79
149
|
});
|
|
80
150
|
|
|
81
151
|
return {
|
|
@@ -321,7 +391,7 @@ class Installer {
|
|
|
321
391
|
/**
|
|
322
392
|
* Set up IDE integrations for each selected IDE.
|
|
323
393
|
*/
|
|
324
|
-
async _setupIdes(config, allModules, paths, addResult) {
|
|
394
|
+
async _setupIdes(config, allModules, paths, addResult, previousSkillIds = new Set()) {
|
|
325
395
|
if (config.skipIde || !config.ides || config.ides.length === 0) return;
|
|
326
396
|
|
|
327
397
|
await this.ideManager.ensureInitialized();
|
|
@@ -336,6 +406,7 @@ class Installer {
|
|
|
336
406
|
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
|
|
337
407
|
selectedModules: allModules || [],
|
|
338
408
|
verbose: config.verbose,
|
|
409
|
+
previousSkillIds,
|
|
339
410
|
});
|
|
340
411
|
|
|
341
412
|
if (setupResult.success) {
|
|
@@ -346,6 +417,33 @@ class Installer {
|
|
|
346
417
|
}
|
|
347
418
|
}
|
|
348
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Remove skill directories from _bmad/ after IDE installation.
|
|
422
|
+
* Skills are self-contained in IDE directories, so _bmad/ only needs
|
|
423
|
+
* module-level files (config.yaml, _config/, etc.).
|
|
424
|
+
* Also cleans up skill dirs left by older installer versions.
|
|
425
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
426
|
+
*/
|
|
427
|
+
async _cleanupSkillDirs(bmadDir) {
|
|
428
|
+
const csv = require('csv-parse/sync');
|
|
429
|
+
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
|
430
|
+
if (!(await fs.pathExists(csvPath))) return;
|
|
431
|
+
|
|
432
|
+
const csvContent = await fs.readFile(csvPath, 'utf8');
|
|
433
|
+
const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true });
|
|
434
|
+
const bmadFolderName = path.basename(bmadDir);
|
|
435
|
+
const bmadPrefix = bmadFolderName + '/';
|
|
436
|
+
|
|
437
|
+
for (const record of records) {
|
|
438
|
+
if (!record.path) continue;
|
|
439
|
+
const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;
|
|
440
|
+
const sourceDir = path.dirname(path.join(bmadDir, relativePath));
|
|
441
|
+
if (await fs.pathExists(sourceDir)) {
|
|
442
|
+
await fs.remove(sourceDir);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
349
447
|
/**
|
|
350
448
|
* Restore custom and modified files that were backed up before the update.
|
|
351
449
|
* No-op for fresh installs (updateState is null).
|
|
@@ -556,7 +654,7 @@ class Installer {
|
|
|
556
654
|
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
|
557
655
|
|
|
558
656
|
const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
|
|
559
|
-
await officialModules.install(
|
|
657
|
+
const installResult = await officialModules.install(
|
|
560
658
|
moduleName,
|
|
561
659
|
paths.bmadDir,
|
|
562
660
|
(filePath) => {
|
|
@@ -570,7 +668,12 @@ class Installer {
|
|
|
570
668
|
},
|
|
571
669
|
);
|
|
572
670
|
|
|
573
|
-
|
|
671
|
+
// Get display name from source module.yaml; version from marketplace.json
|
|
672
|
+
const sourcePath = await officialModules.findModuleSource(moduleName, { silent: true });
|
|
673
|
+
const moduleInfo = sourcePath ? await officialModules.getModuleInfo(sourcePath, moduleName, '') : null;
|
|
674
|
+
const displayName = moduleInfo?.name || moduleName;
|
|
675
|
+
const version = sourcePath ? await this._getMarketplaceVersion(sourcePath) : '';
|
|
676
|
+
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
|
|
574
677
|
}
|
|
575
678
|
}
|
|
576
679
|
|
|
@@ -598,7 +701,11 @@ class Installer {
|
|
|
598
701
|
[moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
|
|
599
702
|
});
|
|
600
703
|
|
|
601
|
-
|
|
704
|
+
// Get display name from source module.yaml; version from marketplace.json
|
|
705
|
+
const moduleInfo = await officialModules.getModuleInfo(sourcePath, moduleName, '');
|
|
706
|
+
const displayName = moduleInfo?.name || moduleName;
|
|
707
|
+
const version = await this._getMarketplaceVersion(sourcePath);
|
|
708
|
+
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
|
|
602
709
|
}
|
|
603
710
|
}
|
|
604
711
|
|
|
@@ -1062,23 +1169,10 @@ class Installer {
|
|
|
1062
1169
|
const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
|
|
1063
1170
|
|
|
1064
1171
|
// Build step lines with status indicators
|
|
1172
|
+
const preVersions = context.preInstallVersions || new Map();
|
|
1065
1173
|
const lines = [];
|
|
1066
1174
|
for (const r of results) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
if (r.status !== 'ok') {
|
|
1070
|
-
stepLabel = r.step;
|
|
1071
|
-
} else if (r.step === 'Core') {
|
|
1072
|
-
stepLabel = 'BMAD';
|
|
1073
|
-
} else if (r.step.startsWith('Module: ')) {
|
|
1074
|
-
stepLabel = r.step;
|
|
1075
|
-
} else if (selectedIdes.has(String(r.step).toLowerCase())) {
|
|
1076
|
-
stepLabel = r.step;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (!stepLabel) {
|
|
1080
|
-
continue;
|
|
1081
|
-
}
|
|
1175
|
+
const stepLabel = r.step;
|
|
1082
1176
|
|
|
1083
1177
|
let icon;
|
|
1084
1178
|
if (r.status === 'ok') {
|
|
@@ -1088,18 +1182,32 @@ class Installer {
|
|
|
1088
1182
|
} else {
|
|
1089
1183
|
icon = color.red('\u2717');
|
|
1090
1184
|
}
|
|
1091
|
-
|
|
1185
|
+
|
|
1186
|
+
// Build version detail for module results
|
|
1187
|
+
let detail = '';
|
|
1188
|
+
if (r.moduleCode && r.newVersion) {
|
|
1189
|
+
const oldVersion = preVersions.get(r.moduleCode);
|
|
1190
|
+
if (oldVersion && oldVersion === r.newVersion) {
|
|
1191
|
+
detail = ` (v${r.newVersion}, no change)`;
|
|
1192
|
+
} else if (oldVersion) {
|
|
1193
|
+
detail = ` (v${oldVersion} → v${r.newVersion})`;
|
|
1194
|
+
} else {
|
|
1195
|
+
detail = ` (v${r.newVersion}, installed)`;
|
|
1196
|
+
}
|
|
1197
|
+
} else if (r.detail) {
|
|
1198
|
+
detail = ` (${r.detail})`;
|
|
1199
|
+
}
|
|
1092
1200
|
lines.push(` ${icon} ${stepLabel}${detail}`);
|
|
1093
1201
|
}
|
|
1094
1202
|
|
|
1095
1203
|
if ((context.ides || []).length === 0) {
|
|
1096
|
-
lines.push(` ${color.green('\u2713')} No IDE selected
|
|
1204
|
+
lines.push(` ${color.green('\u2713')} No IDE selected (installed in _bmad only)`);
|
|
1097
1205
|
}
|
|
1098
1206
|
|
|
1099
1207
|
// Context and warnings
|
|
1100
1208
|
lines.push('');
|
|
1101
1209
|
if (context.bmadDir) {
|
|
1102
|
-
lines.push(` Installed to: ${
|
|
1210
|
+
lines.push(` Installed to: ${context.bmadDir}`);
|
|
1103
1211
|
}
|
|
1104
1212
|
if (context.customFiles && context.customFiles.length > 0) {
|
|
1105
1213
|
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
|
|
@@ -1111,17 +1219,18 @@ class Installer {
|
|
|
1111
1219
|
// Next steps
|
|
1112
1220
|
lines.push(
|
|
1113
1221
|
'',
|
|
1114
|
-
'
|
|
1115
|
-
`
|
|
1116
|
-
`
|
|
1117
|
-
|
|
1118
|
-
`
|
|
1222
|
+
' Get started:',
|
|
1223
|
+
` 1. Launch your AI agent from your project folder`,
|
|
1224
|
+
` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
|
|
1225
|
+
'',
|
|
1226
|
+
` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
|
|
1227
|
+
` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
|
|
1119
1228
|
);
|
|
1120
|
-
if (context.ides && context.ides.length > 0) {
|
|
1121
|
-
lines.push(` Invoke the ${color.cyan('bmad-help')} skill in your IDE Agent to get started`);
|
|
1122
|
-
}
|
|
1123
1229
|
|
|
1124
|
-
await prompts.
|
|
1230
|
+
await prompts.box(lines.join('\n'), 'BMAD is ready to use!', {
|
|
1231
|
+
rounded: true,
|
|
1232
|
+
formatBorder: color.green,
|
|
1233
|
+
});
|
|
1125
1234
|
}
|
|
1126
1235
|
|
|
1127
1236
|
/**
|
|
@@ -1231,6 +1340,7 @@ class Installer {
|
|
|
1231
1340
|
}
|
|
1232
1341
|
|
|
1233
1342
|
for (const moduleName of modulesToUpdate) {
|
|
1343
|
+
if (moduleName === 'core') continue; // Already collected above
|
|
1234
1344
|
const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
|
|
1235
1345
|
if (modulePrompted) {
|
|
1236
1346
|
promptedForNewFields = true;
|
|
@@ -9,7 +9,6 @@ const {
|
|
|
9
9
|
loadSkillManifest: loadSkillManifestShared,
|
|
10
10
|
getCanonicalId: getCanonicalIdShared,
|
|
11
11
|
getArtifactType: getArtifactTypeShared,
|
|
12
|
-
getInstallToBmad: getInstallToBmadShared,
|
|
13
12
|
} = require('../ide/shared/skill-manifest');
|
|
14
13
|
|
|
15
14
|
// Load package.json for version info
|
|
@@ -42,11 +41,6 @@ class ManifestGenerator {
|
|
|
42
41
|
return getArtifactTypeShared(manifest, filename);
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
/** Delegate to shared skill-manifest module */
|
|
46
|
-
getInstallToBmad(manifest, filename) {
|
|
47
|
-
return getInstallToBmadShared(manifest, filename);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
44
|
/**
|
|
51
45
|
* Clean text for CSV output by normalizing whitespace.
|
|
52
46
|
* Note: Quote escaping is handled by escapeCsv() at write time.
|
|
@@ -127,7 +121,7 @@ class ManifestGenerator {
|
|
|
127
121
|
* Recursively walk a module directory tree, collecting native SKILL.md entrypoints.
|
|
128
122
|
* A directory is discovered as a skill when it contains a SKILL.md file with
|
|
129
123
|
* valid name/description frontmatter (name must match directory name).
|
|
130
|
-
* Manifest YAML is loaded only when present — for
|
|
124
|
+
* Manifest YAML is loaded only when present — for agent metadata.
|
|
131
125
|
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
|
|
132
126
|
*/
|
|
133
127
|
async collectSkills() {
|
|
@@ -156,7 +150,7 @@ class ManifestGenerator {
|
|
|
156
150
|
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
|
|
157
151
|
|
|
158
152
|
if (skillMeta) {
|
|
159
|
-
// Load manifest when present (for
|
|
153
|
+
// Load manifest when present (for agent metadata)
|
|
160
154
|
const manifest = await this.loadSkillManifest(dir);
|
|
161
155
|
const artifactType = this.getArtifactType(manifest, skillFile);
|
|
162
156
|
|
|
@@ -182,7 +176,6 @@ class ManifestGenerator {
|
|
|
182
176
|
module: moduleName,
|
|
183
177
|
path: installPath,
|
|
184
178
|
canonicalId,
|
|
185
|
-
install_to_bmad: this.getInstallToBmad(manifest, skillFile),
|
|
186
179
|
});
|
|
187
180
|
|
|
188
181
|
// Add to files list
|
|
@@ -472,7 +465,7 @@ class ManifestGenerator {
|
|
|
472
465
|
const csvPath = path.join(cfgDir, 'skill-manifest.csv');
|
|
473
466
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
474
467
|
|
|
475
|
-
let csvContent = 'canonicalId,name,description,module,path
|
|
468
|
+
let csvContent = 'canonicalId,name,description,module,path\n';
|
|
476
469
|
|
|
477
470
|
for (const skill of this.skills) {
|
|
478
471
|
const row = [
|
|
@@ -481,7 +474,6 @@ class ManifestGenerator {
|
|
|
481
474
|
escapeCsv(skill.description),
|
|
482
475
|
escapeCsv(skill.module),
|
|
483
476
|
escapeCsv(skill.path),
|
|
484
|
-
escapeCsv(skill.install_to_bmad),
|
|
485
477
|
].join(',');
|
|
486
478
|
csvContent += row + '\n';
|
|
487
479
|
}
|
|
@@ -837,14 +837,13 @@ class Manifest {
|
|
|
837
837
|
* @returns {Object} Version info object with version, source, npmPackage, repoUrl
|
|
838
838
|
*/
|
|
839
839
|
async getModuleVersionInfo(moduleName, bmadDir, moduleSourcePath = null) {
|
|
840
|
-
const os = require('node:os');
|
|
841
840
|
const yaml = require('yaml');
|
|
842
841
|
|
|
843
|
-
//
|
|
842
|
+
// Resolve source type first, then read version with the correct path context
|
|
844
843
|
if (['core', 'bmm'].includes(moduleName)) {
|
|
845
|
-
const
|
|
844
|
+
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
|
846
845
|
return {
|
|
847
|
-
version
|
|
846
|
+
version,
|
|
848
847
|
source: 'built-in',
|
|
849
848
|
npmPackage: null,
|
|
850
849
|
repoUrl: null,
|
|
@@ -857,42 +856,20 @@ class Manifest {
|
|
|
857
856
|
const moduleInfo = await extMgr.getModuleByCode(moduleName);
|
|
858
857
|
|
|
859
858
|
if (moduleInfo) {
|
|
860
|
-
// External module
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (moduleInfo.npmPackage) {
|
|
864
|
-
// Fetch version from npm registry
|
|
865
|
-
try {
|
|
866
|
-
version = await this.fetchNpmVersion(moduleInfo.npmPackage);
|
|
867
|
-
} catch {
|
|
868
|
-
// npm fetch failed, try cache as fallback
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// If npm didn't work, try reading from cached repo's package.json
|
|
873
|
-
if (!version) {
|
|
874
|
-
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
|
|
875
|
-
const packageJsonPath = path.join(cacheDir, 'package.json');
|
|
876
|
-
|
|
877
|
-
if (await fs.pathExists(packageJsonPath)) {
|
|
878
|
-
try {
|
|
879
|
-
const pkg = require(packageJsonPath);
|
|
880
|
-
version = pkg.version;
|
|
881
|
-
} catch (error) {
|
|
882
|
-
await prompts.log.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
859
|
+
// External module: use moduleSourcePath if provided, otherwise fall back to cache
|
|
860
|
+
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
|
887
861
|
return {
|
|
888
|
-
version
|
|
862
|
+
version,
|
|
889
863
|
source: 'external',
|
|
890
864
|
npmPackage: moduleInfo.npmPackage || null,
|
|
891
865
|
repoUrl: moduleInfo.url || null,
|
|
892
866
|
};
|
|
893
867
|
}
|
|
894
868
|
|
|
895
|
-
// Custom module
|
|
869
|
+
// Custom module: resolve path from source or cache before reading version
|
|
870
|
+
const customSourcePath = moduleSourcePath || path.join(bmadDir, '_config', 'custom', moduleName);
|
|
871
|
+
const version = await this._readMarketplaceVersion(moduleName, customSourcePath);
|
|
872
|
+
|
|
896
873
|
const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName);
|
|
897
874
|
const moduleYamlPath = path.join(cacheDir, 'module.yaml');
|
|
898
875
|
|
|
@@ -901,7 +878,7 @@ class Manifest {
|
|
|
901
878
|
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
|
902
879
|
const moduleConfig = yaml.parse(yamlContent);
|
|
903
880
|
return {
|
|
904
|
-
version: moduleConfig.version || null,
|
|
881
|
+
version: version || moduleConfig.version || null,
|
|
905
882
|
source: 'custom',
|
|
906
883
|
npmPackage: moduleConfig.npmPackage || null,
|
|
907
884
|
repoUrl: moduleConfig.repoUrl || null,
|
|
@@ -913,13 +890,62 @@ class Manifest {
|
|
|
913
890
|
|
|
914
891
|
// Unknown module
|
|
915
892
|
return {
|
|
916
|
-
version
|
|
893
|
+
version,
|
|
917
894
|
source: 'unknown',
|
|
918
895
|
npmPackage: null,
|
|
919
896
|
repoUrl: null,
|
|
920
897
|
};
|
|
921
898
|
}
|
|
922
899
|
|
|
900
|
+
/**
|
|
901
|
+
* Read version from .claude-plugin/marketplace.json for a module
|
|
902
|
+
* @param {string} moduleName - Module code
|
|
903
|
+
* @returns {string|null} Version or null
|
|
904
|
+
*/
|
|
905
|
+
async _readMarketplaceVersion(moduleName, moduleSourcePath = null) {
|
|
906
|
+
const os = require('node:os');
|
|
907
|
+
let marketplacePath;
|
|
908
|
+
|
|
909
|
+
if (['core', 'bmm'].includes(moduleName)) {
|
|
910
|
+
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
|
911
|
+
} else if (moduleSourcePath) {
|
|
912
|
+
// Walk up from source path to find marketplace.json
|
|
913
|
+
let dir = moduleSourcePath;
|
|
914
|
+
for (let i = 0; i < 5; i++) {
|
|
915
|
+
const candidate = path.join(dir, '.claude-plugin', 'marketplace.json');
|
|
916
|
+
if (await fs.pathExists(candidate)) {
|
|
917
|
+
marketplacePath = candidate;
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
const parent = path.dirname(dir);
|
|
921
|
+
if (parent === dir) break;
|
|
922
|
+
dir = parent;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Fallback to external module cache
|
|
927
|
+
if (!marketplacePath) {
|
|
928
|
+
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
|
|
929
|
+
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
try {
|
|
933
|
+
if (await fs.pathExists(marketplacePath)) {
|
|
934
|
+
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
|
935
|
+
const plugins = data?.plugins;
|
|
936
|
+
if (!Array.isArray(plugins) || plugins.length === 0) return null;
|
|
937
|
+
let best = null;
|
|
938
|
+
for (const p of plugins) {
|
|
939
|
+
if (p.version && (!best || p.version > best)) best = p.version;
|
|
940
|
+
}
|
|
941
|
+
return best;
|
|
942
|
+
}
|
|
943
|
+
} catch {
|
|
944
|
+
// ignore
|
|
945
|
+
}
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
|
|
923
949
|
/**
|
|
924
950
|
* Fetch latest version from npm for a package
|
|
925
951
|
* @param {string} packageName - npm package name
|
|
@@ -86,7 +86,7 @@ class ConfigDrivenIdeSetup {
|
|
|
86
86
|
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
|
|
87
87
|
|
|
88
88
|
// Clean up any old BMAD installation first
|
|
89
|
-
await this.cleanup(projectDir, options);
|
|
89
|
+
await this.cleanup(projectDir, options, bmadDir);
|
|
90
90
|
|
|
91
91
|
if (!this.installerConfig) {
|
|
92
92
|
return { success: false, reason: 'no-config' };
|
|
@@ -183,18 +183,6 @@ class ConfigDrivenIdeSetup {
|
|
|
183
183
|
count++;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
// Post-install cleanup: remove _bmad/ directories for skills with install_to_bmad === "false"
|
|
187
|
-
for (const record of records) {
|
|
188
|
-
if (record.install_to_bmad === 'false') {
|
|
189
|
-
const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;
|
|
190
|
-
const sourceFile = path.join(bmadDir, relativePath);
|
|
191
|
-
const sourceDir = path.dirname(sourceFile);
|
|
192
|
-
if (await fs.pathExists(sourceDir)) {
|
|
193
|
-
await fs.remove(sourceDir);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
186
|
return count;
|
|
199
187
|
}
|
|
200
188
|
|
|
@@ -215,15 +203,34 @@ class ConfigDrivenIdeSetup {
|
|
|
215
203
|
* Cleanup IDE configuration
|
|
216
204
|
* @param {string} projectDir - Project directory
|
|
217
205
|
*/
|
|
218
|
-
async cleanup(projectDir, options = {}) {
|
|
206
|
+
async cleanup(projectDir, options = {}, bmadDir = null) {
|
|
207
|
+
const resolvedBmadDir = bmadDir || (await this._findBmadDir(projectDir));
|
|
208
|
+
|
|
209
|
+
// Build removal set: previously installed skills + removals.txt entries
|
|
210
|
+
let removalSet;
|
|
211
|
+
if (options.previousSkillIds && options.previousSkillIds.size > 0) {
|
|
212
|
+
// Install/update flow: use pre-captured skill IDs (before manifest was overwritten)
|
|
213
|
+
removalSet = new Set(options.previousSkillIds);
|
|
214
|
+
if (resolvedBmadDir) {
|
|
215
|
+
const removals = await this.loadRemovalLists(resolvedBmadDir);
|
|
216
|
+
for (const entry of removals) removalSet.add(entry);
|
|
217
|
+
}
|
|
218
|
+
} else if (resolvedBmadDir) {
|
|
219
|
+
// Uninstall flow: read from current skill-manifest.csv + removals.txt
|
|
220
|
+
removalSet = await this._buildUninstallSet(resolvedBmadDir);
|
|
221
|
+
} else {
|
|
222
|
+
removalSet = new Set();
|
|
223
|
+
}
|
|
224
|
+
|
|
219
225
|
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
|
|
226
|
+
// Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
|
|
220
227
|
if (this.installerConfig?.legacy_targets) {
|
|
221
228
|
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
|
222
229
|
for (const legacyDir of this.installerConfig.legacy_targets) {
|
|
223
230
|
if (this.isGlobalPath(legacyDir)) {
|
|
224
231
|
await this.warnGlobalLegacy(legacyDir, options);
|
|
225
232
|
} else {
|
|
226
|
-
await this.cleanupTarget(projectDir, legacyDir, options);
|
|
233
|
+
await this.cleanupTarget(projectDir, legacyDir, options, null);
|
|
227
234
|
await this.removeEmptyParents(projectDir, legacyDir);
|
|
228
235
|
}
|
|
229
236
|
}
|
|
@@ -244,9 +251,9 @@ class ConfigDrivenIdeSetup {
|
|
|
244
251
|
await this.cleanupRovoDevPrompts(projectDir, options);
|
|
245
252
|
}
|
|
246
253
|
|
|
247
|
-
// Clean target directory
|
|
254
|
+
// Clean current target directory
|
|
248
255
|
if (this.installerConfig?.target_dir) {
|
|
249
|
-
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options);
|
|
256
|
+
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet);
|
|
250
257
|
}
|
|
251
258
|
}
|
|
252
259
|
|
|
@@ -286,23 +293,117 @@ class ConfigDrivenIdeSetup {
|
|
|
286
293
|
}
|
|
287
294
|
|
|
288
295
|
/**
|
|
289
|
-
*
|
|
296
|
+
* Find the _bmad directory in a project
|
|
297
|
+
* @param {string} projectDir - Project directory
|
|
298
|
+
* @returns {string|null} Path to bmad dir or null
|
|
299
|
+
*/
|
|
300
|
+
async _findBmadDir(projectDir) {
|
|
301
|
+
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
|
302
|
+
return (await fs.pathExists(bmadDir)) ? bmadDir : null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Build the full set of entries to remove for uninstall.
|
|
307
|
+
* Reads skill-manifest.csv to know exactly what was installed, plus removal lists.
|
|
308
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
309
|
+
* @returns {Set<string>} Set of entries to remove
|
|
310
|
+
*/
|
|
311
|
+
async _buildUninstallSet(bmadDir) {
|
|
312
|
+
const removals = await this.loadRemovalLists(bmadDir);
|
|
313
|
+
|
|
314
|
+
// Also add all currently installed skills from skill-manifest.csv
|
|
315
|
+
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
|
316
|
+
try {
|
|
317
|
+
if (await fs.pathExists(csvPath)) {
|
|
318
|
+
const content = await fs.readFile(csvPath, 'utf8');
|
|
319
|
+
const records = csv.parse(content, { columns: true, skip_empty_lines: true });
|
|
320
|
+
for (const record of records) {
|
|
321
|
+
if (record.canonicalId) {
|
|
322
|
+
removals.add(record.canonicalId);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
// If we can't read the manifest, we still have the removal lists
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return removals;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Load removal lists from all module sources in the bmad directory.
|
|
335
|
+
* Each module can have an optional removals.txt listing entries to remove.
|
|
336
|
+
* @param {string} bmadDir - BMAD installation directory
|
|
337
|
+
* @returns {Set<string>} Set of entries to remove
|
|
338
|
+
*/
|
|
339
|
+
async loadRemovalLists(bmadDir) {
|
|
340
|
+
const removals = new Set();
|
|
341
|
+
const { getProjectRoot } = require('../project-root');
|
|
342
|
+
|
|
343
|
+
// Read project-level removals.txt (covers core and bmm)
|
|
344
|
+
const projectRemovalsPath = path.join(getProjectRoot(), 'removals.txt');
|
|
345
|
+
await this._readRemovalFile(projectRemovalsPath, removals);
|
|
346
|
+
|
|
347
|
+
// Read per-module removals.txt from installed module directories
|
|
348
|
+
try {
|
|
349
|
+
const entries = await fs.readdir(bmadDir);
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
if (entry.startsWith('_')) continue;
|
|
352
|
+
const removalPath = path.join(bmadDir, entry, 'removals.txt');
|
|
353
|
+
await this._readRemovalFile(removalPath, removals);
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
// bmadDir may not exist yet on fresh install
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return removals;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Read a removals.txt file and add entries to the set
|
|
364
|
+
* @param {string} filePath - Path to removals.txt
|
|
365
|
+
* @param {Set<string>} removals - Set to add entries to
|
|
366
|
+
*/
|
|
367
|
+
async _readRemovalFile(filePath, removals) {
|
|
368
|
+
try {
|
|
369
|
+
if (await fs.pathExists(filePath)) {
|
|
370
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
371
|
+
for (const line of content.split('\n')) {
|
|
372
|
+
const trimmed = line.trim();
|
|
373
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
374
|
+
removals.add(trimmed);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
// Optional file — ignore errors
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Cleanup a specific target directory.
|
|
385
|
+
* When removalSet is provided, only removes entries in that set.
|
|
386
|
+
* When removalSet is null (legacy dirs), removes all bmad-prefixed entries.
|
|
290
387
|
* @param {string} projectDir - Project directory
|
|
291
388
|
* @param {string} targetDir - Target directory to clean
|
|
389
|
+
* @param {Object} options - Cleanup options
|
|
390
|
+
* @param {Set<string>|null} removalSet - Entries to remove, or null for legacy prefix matching
|
|
292
391
|
*/
|
|
293
|
-
async cleanupTarget(projectDir, targetDir, options = {}) {
|
|
392
|
+
async cleanupTarget(projectDir, targetDir, options = {}, removalSet = new Set()) {
|
|
294
393
|
const targetPath = path.join(projectDir, targetDir);
|
|
295
394
|
|
|
296
395
|
if (!(await fs.pathExists(targetPath))) {
|
|
297
396
|
return;
|
|
298
397
|
}
|
|
299
398
|
|
|
300
|
-
|
|
399
|
+
if (removalSet && removalSet.size === 0) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
301
403
|
let entries;
|
|
302
404
|
try {
|
|
303
405
|
entries = await fs.readdir(targetPath);
|
|
304
406
|
} catch {
|
|
305
|
-
// Directory exists but can't be read - skip cleanup
|
|
306
407
|
return;
|
|
307
408
|
}
|
|
308
409
|
|
|
@@ -313,23 +414,26 @@ class ConfigDrivenIdeSetup {
|
|
|
313
414
|
let removedCount = 0;
|
|
314
415
|
|
|
315
416
|
for (const entry of entries) {
|
|
316
|
-
if (!entry || typeof entry !== 'string')
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (entry.startsWith('bmad
|
|
320
|
-
|
|
417
|
+
if (!entry || typeof entry !== 'string') continue;
|
|
418
|
+
|
|
419
|
+
// Always preserve bmad-os-* utility skills regardless of cleanup mode
|
|
420
|
+
if (entry.startsWith('bmad-os-')) continue;
|
|
421
|
+
|
|
422
|
+
// Surgical removal from set, or legacy prefix matching when set is null
|
|
423
|
+
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
|
|
424
|
+
|
|
425
|
+
if (shouldRemove) {
|
|
321
426
|
try {
|
|
322
|
-
await fs.remove(
|
|
427
|
+
await fs.remove(path.join(targetPath, entry));
|
|
323
428
|
removedCount++;
|
|
324
429
|
} catch {
|
|
325
|
-
// Skip entries that can't be removed
|
|
430
|
+
// Skip entries that can't be removed
|
|
326
431
|
}
|
|
327
432
|
}
|
|
328
433
|
}
|
|
329
434
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
435
|
+
// Only log cleanup when it's not a routine reinstall (legacy dir cleanup or actual removals)
|
|
436
|
+
// Suppress for current target_dir since it's always cleaned before a fresh write
|
|
333
437
|
|
|
334
438
|
// Remove empty directory after cleanup
|
|
335
439
|
if (removedCount > 0) {
|
|
@@ -339,7 +443,7 @@ class ConfigDrivenIdeSetup {
|
|
|
339
443
|
await fs.remove(targetPath);
|
|
340
444
|
}
|
|
341
445
|
} catch {
|
|
342
|
-
// Directory may already be gone or in use
|
|
446
|
+
// Directory may already be gone or in use
|
|
343
447
|
}
|
|
344
448
|
}
|
|
345
449
|
}
|
|
@@ -54,19 +54,4 @@ function getArtifactType(manifest, filename) {
|
|
|
54
54
|
return null;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
* Get the install_to_bmad flag for a specific file from a loaded skill manifest.
|
|
59
|
-
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)
|
|
60
|
-
* @param {string} filename - Source filename to look up
|
|
61
|
-
* @returns {boolean} install_to_bmad value (defaults to true)
|
|
62
|
-
*/
|
|
63
|
-
function getInstallToBmad(manifest, filename) {
|
|
64
|
-
if (!manifest) return true;
|
|
65
|
-
// Single-entry manifest applies to all files in the directory
|
|
66
|
-
if (manifest.__single) return manifest.__single.install_to_bmad !== false;
|
|
67
|
-
// Multi-entry: look up by filename directly
|
|
68
|
-
if (manifest[filename]) return manifest[filename].install_to_bmad !== false;
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
module.exports = { loadSkillManifest, getCanonicalId, getArtifactType, getInstallToBmad };
|
|
57
|
+
module.exports = { loadSkillManifest, getCanonicalId, getArtifactType };
|
|
@@ -6,32 +6,25 @@
|
|
|
6
6
|
startMessage: |
|
|
7
7
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
|
|
29
|
-
- Subscribe on YouTube: https://www.youtube.com/@BMadCode
|
|
30
|
-
- Free Community and Support: https://discord.gg/gk8jAdXWmj
|
|
31
|
-
- Donate: https://buymeacoffee.com/bmad
|
|
32
|
-
- Corporate Sponsorship available
|
|
33
|
-
|
|
34
|
-
Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/blob/main/CHANGELOG.md
|
|
9
|
+
Agile AI-Driven Development. Powered by BMad Core and a growing module ecosystem.
|
|
10
|
+
Install official and community modules during setup to customize your experience.
|
|
11
|
+
|
|
12
|
+
🌟 100% free. 100% open source. Always.
|
|
13
|
+
No paywalls. No gated content. Knowledge shared, not sold.
|
|
14
|
+
|
|
15
|
+
🌐 CONNECT:
|
|
16
|
+
Website: https://bmadcode.com/
|
|
17
|
+
Discord: https://discord.gg/gk8jAdXWmj
|
|
18
|
+
YouTube: https://www.youtube.com/@BMadCode
|
|
19
|
+
X: https://x.com/BMadCode
|
|
20
|
+
Facebook: https://facebook.com/@BMadCode
|
|
21
|
+
|
|
22
|
+
⭐ SUPPORT THE PROJECT:
|
|
23
|
+
Star us: https://github.com/bmad-code-org/BMAD-METHOD/
|
|
24
|
+
Donate: https://buymeacoffee.com/bmad
|
|
25
|
+
Corporate sponsorship and speaking inquiries: contact@bmadcode.com
|
|
26
|
+
|
|
27
|
+
Docs, blog, and latest updates: https://bmadcode.com/
|
|
35
28
|
|
|
36
29
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
37
30
|
|
package/tools/installer/ui.js
CHANGED
|
@@ -4,8 +4,50 @@ const fs = require('fs-extra');
|
|
|
4
4
|
const { CLIUtils } = require('./cli-utils');
|
|
5
5
|
const { CustomHandler } = require('./custom-handler');
|
|
6
6
|
const { ExternalModuleManager } = require('./modules/external-manager');
|
|
7
|
+
const { getProjectRoot } = require('./project-root');
|
|
7
8
|
const prompts = require('./prompts');
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Read module version from .claude-plugin/marketplace.json
|
|
12
|
+
* @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis')
|
|
13
|
+
* @returns {string} Version string or empty string
|
|
14
|
+
*/
|
|
15
|
+
async function getMarketplaceVersion(moduleCode) {
|
|
16
|
+
let marketplacePath;
|
|
17
|
+
if (moduleCode === 'core' || moduleCode === 'bmm') {
|
|
18
|
+
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
|
19
|
+
} else {
|
|
20
|
+
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleCode);
|
|
21
|
+
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
if (await fs.pathExists(marketplacePath)) {
|
|
25
|
+
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
|
26
|
+
return _extractMarketplaceVersion(data);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// ignore
|
|
30
|
+
}
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract the highest version from marketplace.json plugins array.
|
|
36
|
+
* Handles multiple plugins per file safely.
|
|
37
|
+
* @param {Object} data - Parsed marketplace.json
|
|
38
|
+
* @returns {string} Version string or empty string
|
|
39
|
+
*/
|
|
40
|
+
function _extractMarketplaceVersion(data) {
|
|
41
|
+
const plugins = data?.plugins;
|
|
42
|
+
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
|
43
|
+
// Use the highest version across all plugins in the file
|
|
44
|
+
let best = '';
|
|
45
|
+
for (const p of plugins) {
|
|
46
|
+
if (p.version && (!best || p.version > best)) best = p.version;
|
|
47
|
+
}
|
|
48
|
+
return best;
|
|
49
|
+
}
|
|
50
|
+
|
|
9
51
|
// Separator class for visual grouping in select/multiselect prompts
|
|
10
52
|
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
|
11
53
|
class Separator {
|
|
@@ -70,17 +112,14 @@ class UI {
|
|
|
70
112
|
if (hasExistingInstall) {
|
|
71
113
|
// Get version information
|
|
72
114
|
const { existingInstall, bmadDir } = await this.getExistingInstallation(confirmedDirectory);
|
|
73
|
-
const packageJsonPath = path.join(__dirname, '../../package.json');
|
|
74
|
-
const currentVersion = require(packageJsonPath).version;
|
|
75
|
-
const installedVersion = existingInstall.installed ? existingInstall.version || 'unknown' : 'unknown';
|
|
76
115
|
|
|
77
116
|
// Build menu choices dynamically
|
|
78
117
|
const choices = [];
|
|
79
118
|
|
|
80
119
|
// Always show Quick Update first (allows refreshing installation even on same version)
|
|
81
|
-
if (
|
|
120
|
+
if (existingInstall.installed) {
|
|
82
121
|
choices.push({
|
|
83
|
-
name:
|
|
122
|
+
name: 'Quick Update',
|
|
84
123
|
value: 'quick-update',
|
|
85
124
|
});
|
|
86
125
|
}
|
|
@@ -880,14 +919,18 @@ class UI {
|
|
|
880
919
|
const lockedValues = ['core'];
|
|
881
920
|
|
|
882
921
|
// Core module is always installed — show it locked at the top
|
|
883
|
-
|
|
922
|
+
const coreVersion = await getMarketplaceVersion('core');
|
|
923
|
+
const coreLabel = coreVersion ? `BMad Core Module (v${coreVersion})` : 'BMad Core Module';
|
|
924
|
+
allOptions.push({ label: coreLabel, value: 'core', hint: 'Core configuration and shared resources' });
|
|
884
925
|
initialValues.push('core');
|
|
885
926
|
|
|
886
927
|
// Helper to build module entry with proper sorting and selection
|
|
887
|
-
const buildModuleEntry = (mod, value, group) => {
|
|
928
|
+
const buildModuleEntry = async (mod, value, group) => {
|
|
888
929
|
const isInstalled = installedModuleIds.has(value);
|
|
930
|
+
const version = await getMarketplaceVersion(value);
|
|
931
|
+
const label = version ? `${mod.name} (v${version})` : mod.name;
|
|
889
932
|
return {
|
|
890
|
-
label
|
|
933
|
+
label,
|
|
891
934
|
value,
|
|
892
935
|
hint: mod.description || group,
|
|
893
936
|
// Pre-select only if already installed (not on fresh install)
|
|
@@ -899,7 +942,7 @@ class UI {
|
|
|
899
942
|
const localEntries = [];
|
|
900
943
|
for (const mod of localModules) {
|
|
901
944
|
if (!mod.isCustom && mod.id !== 'core') {
|
|
902
|
-
const entry = buildModuleEntry(mod, mod.id, 'Local');
|
|
945
|
+
const entry = await buildModuleEntry(mod, mod.id, 'Local');
|
|
903
946
|
localEntries.push(entry);
|
|
904
947
|
if (entry.selected) {
|
|
905
948
|
initialValues.push(mod.id);
|
|
@@ -912,7 +955,7 @@ class UI {
|
|
|
912
955
|
const officialModules = [];
|
|
913
956
|
for (const mod of externalModules) {
|
|
914
957
|
if (mod.type === 'bmad-org') {
|
|
915
|
-
const entry = buildModuleEntry(mod, mod.code, 'Official');
|
|
958
|
+
const entry = await buildModuleEntry(mod, mod.code, 'Official');
|
|
916
959
|
officialModules.push(entry);
|
|
917
960
|
if (entry.selected) {
|
|
918
961
|
initialValues.push(mod.code);
|
|
@@ -925,7 +968,7 @@ class UI {
|
|
|
925
968
|
const communityModules = [];
|
|
926
969
|
for (const mod of externalModules) {
|
|
927
970
|
if (mod.type === 'community') {
|
|
928
|
-
const entry = buildModuleEntry(mod, mod.code, 'Community');
|
|
971
|
+
const entry = await buildModuleEntry(mod, mod.code, 'Community');
|
|
929
972
|
communityModules.push(entry);
|
|
930
973
|
if (entry.selected) {
|
|
931
974
|
initialValues.push(mod.code);
|