Compare commits
5 Commits
100bb2cc03
...
6192bb69eb
| Author | SHA1 | Date |
|---|---|---|
|
|
6192bb69eb | |
|
|
6cecab2626 | |
|
|
e076e489ae | |
|
|
2a3171d66a | |
|
|
c46502f640 |
|
|
@ -0,0 +1,17 @@
|
||||||
|
# BMad Method - Skill Removal List
|
||||||
|
# Entries listed here will be removed from IDE skill directories during install/update.
|
||||||
|
# One entry per line. Lines starting with # are comments.
|
||||||
|
# Each entry is a skill directory name (canonicalId) that was removed or renamed.
|
||||||
|
|
||||||
|
# Removed agents (v6.2.0 - v6.2.2)
|
||||||
|
bmad-agent-sm
|
||||||
|
bmad-agent-qa
|
||||||
|
bmad-agent-quick-flow-solo-dev
|
||||||
|
|
||||||
|
# Removed skills (v6.2.0 - v6.2.2)
|
||||||
|
bmad-create-product-brief
|
||||||
|
bmad-product-brief-preview
|
||||||
|
bmad-quick-spec
|
||||||
|
bmad-quick-flow
|
||||||
|
bmad-quick-dev-new-preview
|
||||||
|
bmad-init
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
# BMAD PRD Purpose
|
||||||
|
|
||||||
|
**The PRD is the top of the required funnel that feeds all subsequent product development work in rhw BMad Method.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is a BMAD PRD?
|
||||||
|
|
||||||
|
A dual-audience document serving:
|
||||||
|
1. **Human Product Managers and builders** - Vision, strategy, stakeholder communication
|
||||||
|
2. **LLM Downstream Consumption** - UX Design → Architecture → Epics → Development AI Agents
|
||||||
|
|
||||||
|
Each successive document becomes more AI-tailored and granular.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Philosophy: Information Density
|
||||||
|
|
||||||
|
**High Signal-to-Noise Ratio**
|
||||||
|
|
||||||
|
Every sentence must carry information weight. LLMs consume precise, dense content efficiently.
|
||||||
|
|
||||||
|
**Anti-Patterns (Eliminate These):**
|
||||||
|
- ❌ "The system will allow users to..." → ✅ "Users can..."
|
||||||
|
- ❌ "It is important to note that..." → ✅ State the fact directly
|
||||||
|
- ❌ "In order to..." → ✅ "To..."
|
||||||
|
- ❌ Conversational filler and padding → ✅ Direct, concise statements
|
||||||
|
|
||||||
|
**Goal:** Maximum information per word. Zero fluff.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Traceability Chain
|
||||||
|
|
||||||
|
**PRD starts the chain:**
|
||||||
|
```
|
||||||
|
Vision → Success Criteria → User Journeys → Functional Requirements → (future: User Stories)
|
||||||
|
```
|
||||||
|
|
||||||
|
**In the PRD, establish:**
|
||||||
|
- Vision → Success Criteria alignment
|
||||||
|
- Success Criteria → User Journey coverage
|
||||||
|
- User Journey → Functional Requirement mapping
|
||||||
|
- All requirements traceable to user needs
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Makes Great Functional Requirements?
|
||||||
|
|
||||||
|
### FRs are Capabilities, Not Implementation
|
||||||
|
|
||||||
|
**Good FR:** "Users can reset their password via email link"
|
||||||
|
**Bad FR:** "System sends JWT via email and validates with database" (implementation leakage)
|
||||||
|
|
||||||
|
**Good FR:** "Dashboard loads in under 2 seconds for 95th percentile"
|
||||||
|
**Bad FR:** "Fast loading time" (subjective, unmeasurable)
|
||||||
|
|
||||||
|
### SMART Quality Criteria
|
||||||
|
|
||||||
|
**Specific:** Clear, precisely defined capability
|
||||||
|
**Measurable:** Quantifiable with test criteria
|
||||||
|
**Attainable:** Realistic within constraints
|
||||||
|
**Relevant:** Aligns with business objectives
|
||||||
|
**Traceable:** Links to source (executive summary or user journey)
|
||||||
|
|
||||||
|
### FR Anti-Patterns
|
||||||
|
|
||||||
|
**Subjective Adjectives:**
|
||||||
|
- ❌ "easy to use", "intuitive", "user-friendly", "fast", "responsive"
|
||||||
|
- ✅ Use metrics: "completes task in under 3 clicks", "loads in under 2 seconds"
|
||||||
|
|
||||||
|
**Implementation Leakage:**
|
||||||
|
- ❌ Technology names, specific libraries, implementation details
|
||||||
|
- ✅ Focus on capability and measurable outcomes
|
||||||
|
|
||||||
|
**Vague Quantifiers:**
|
||||||
|
- ❌ "multiple users", "several options", "various formats"
|
||||||
|
- ✅ "up to 100 concurrent users", "3-5 options", "PDF, DOCX, TXT formats"
|
||||||
|
|
||||||
|
**Missing Test Criteria:**
|
||||||
|
- ❌ "The system shall provide notifications"
|
||||||
|
- ✅ "The system shall send email notifications within 30 seconds of trigger event"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Makes Great Non-Functional Requirements?
|
||||||
|
|
||||||
|
### NFRs Must Be Measurable
|
||||||
|
|
||||||
|
**Template:**
|
||||||
|
```
|
||||||
|
"The system shall [metric] [condition] [measurement method]"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- ✅ "The system shall respond to API requests in under 200ms for 95th percentile as measured by APM monitoring"
|
||||||
|
- ✅ "The system shall maintain 99.9% uptime during business hours as measured by cloud provider SLA"
|
||||||
|
- ✅ "The system shall support 10,000 concurrent users as measured by load testing"
|
||||||
|
|
||||||
|
### NFR Anti-Patterns
|
||||||
|
|
||||||
|
**Unmeasurable Claims:**
|
||||||
|
- ❌ "The system shall be scalable" → ✅ "The system shall handle 10x load growth through horizontal scaling"
|
||||||
|
- ❌ "High availability required" → ✅ "99.9% uptime as measured by cloud provider SLA"
|
||||||
|
|
||||||
|
**Missing Context:**
|
||||||
|
- ❌ "Response time under 1 second" → ✅ "API response time under 1 second for 95th percentile under normal load"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Domain-Specific Requirements
|
||||||
|
|
||||||
|
**Auto-Detect and Enforce Based on Project Context**
|
||||||
|
|
||||||
|
Certain industries have mandatory requirements that must be present:
|
||||||
|
|
||||||
|
- **Healthcare:** HIPAA Privacy & Security Rules, PHI encryption, audit logging, MFA
|
||||||
|
- **Fintech:** PCI-DSS Level 1, AML/KYC compliance, SOX controls, financial audit trails
|
||||||
|
- **GovTech:** NIST framework, Section 508 accessibility (WCAG 2.1 AA), FedRAMP, data residency
|
||||||
|
- **E-Commerce:** PCI-DSS for payments, inventory accuracy, tax calculation by jurisdiction
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Document Structure (Markdown, Human-Readable)
|
||||||
|
|
||||||
|
### Required Sections
|
||||||
|
1. **Executive Summary** - Vision, differentiator, target users
|
||||||
|
2. **Success Criteria** - Measurable outcomes (SMART)
|
||||||
|
3. **Product Scope** - MVP, Growth, Vision phases
|
||||||
|
4. **User Journeys** - Comprehensive coverage
|
||||||
|
5. **Domain Requirements** - Industry-specific compliance (if applicable)
|
||||||
|
6. **Innovation Analysis** - Competitive differentiation (if applicable)
|
||||||
|
7. **Project-Type Requirements** - Platform-specific needs
|
||||||
|
8. **Functional Requirements** - Capability contract (FRs)
|
||||||
|
9. **Non-Functional Requirements** - Quality attributes (NFRs)
|
||||||
|
|
||||||
|
### Formatting for Dual Consumption
|
||||||
|
|
||||||
|
**For Humans:**
|
||||||
|
- Clear, professional language
|
||||||
|
- Logical flow from vision to requirements
|
||||||
|
- Easy for stakeholders to review and approve
|
||||||
|
|
||||||
|
**For LLMs:**
|
||||||
|
- ## Level 2 headers for all main sections (enables extraction)
|
||||||
|
- Consistent structure and patterns
|
||||||
|
- Precise, testable language
|
||||||
|
- High information density
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Downstream Impact
|
||||||
|
|
||||||
|
**How the PRD Feeds Next Artifacts:**
|
||||||
|
|
||||||
|
**UX Design:**
|
||||||
|
- User journeys → interaction flows
|
||||||
|
- FRs → design requirements
|
||||||
|
- Success criteria → UX metrics
|
||||||
|
|
||||||
|
**Architecture:**
|
||||||
|
- FRs → system capabilities
|
||||||
|
- NFRs → architecture decisions
|
||||||
|
- Domain requirements → compliance architecture
|
||||||
|
- Project-type requirements → platform choices
|
||||||
|
|
||||||
|
**Epics & Stories (created after architecture):**
|
||||||
|
- FRs → user stories (1 FR could map to 1-3 stories potentially)
|
||||||
|
- Acceptance criteria → story acceptance tests
|
||||||
|
- Priority → sprint sequencing
|
||||||
|
- Traceability → stories map back to vision
|
||||||
|
|
||||||
|
**Development AI Agents:**
|
||||||
|
- Precise requirements → implementation clarity
|
||||||
|
- Test criteria → automated test generation
|
||||||
|
- Domain requirements → compliance enforcement
|
||||||
|
- Measurable NFRs → performance targets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary: What Makes a Great BMAD PRD?
|
||||||
|
|
||||||
|
✅ **High Information Density** - Every sentence carries weight, zero fluff
|
||||||
|
✅ **Measurable Requirements** - All FRs and NFRs are testable with specific criteria
|
||||||
|
✅ **Clear Traceability** - Each requirement links to user need and business objective
|
||||||
|
✅ **Domain Awareness** - Industry-specific requirements auto-detected and included
|
||||||
|
✅ **Zero Anti-Patterns** - No subjective adjectives, implementation leakage, or vague quantifiers
|
||||||
|
✅ **Dual Audience Optimized** - Human-readable AND LLM-consumable
|
||||||
|
✅ **Markdown Format** - Professional, clean, accessible to all stakeholders
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
# File references (ONLY variables used in this step)
|
# File references (ONLY variables used in this step)
|
||||||
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
|
prdPurpose: '../data/prd-purpose.md'
|
||||||
---
|
---
|
||||||
|
|
||||||
# Step E-1: Discovery & Understanding
|
# Step E-1: Discovery & Understanding
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
# File references (ONLY variables used in this step)
|
# File references (ONLY variables used in this step)
|
||||||
prdFile: '{prd_file_path}'
|
prdFile: '{prd_file_path}'
|
||||||
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
|
prdPurpose: '../data/prd-purpose.md'
|
||||||
---
|
---
|
||||||
|
|
||||||
# Step E-1B: Legacy PRD Conversion Assessment
|
# Step E-1B: Legacy PRD Conversion Assessment
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# File references (ONLY variables used in this step)
|
# File references (ONLY variables used in this step)
|
||||||
prdFile: '{prd_file_path}'
|
prdFile: '{prd_file_path}'
|
||||||
validationReport: '{validation_report_path}' # If provided
|
validationReport: '{validation_report_path}' # If provided
|
||||||
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
|
prdPurpose: '../data/prd-purpose.md'
|
||||||
---
|
---
|
||||||
|
|
||||||
# Step E-2: Deep Review & Analysis
|
# Step E-2: Deep Review & Analysis
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
# File references (ONLY variables used in this step)
|
# File references (ONLY variables used in this step)
|
||||||
prdFile: '{prd_file_path}'
|
prdFile: '{prd_file_path}'
|
||||||
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
|
prdPurpose: '../data/prd-purpose.md'
|
||||||
---
|
---
|
||||||
|
|
||||||
# Step E-3: Edit & Update
|
# Step E-3: Edit & Update
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
---
|
---
|
||||||
# File references (ONLY variables used in this step)
|
# File references (ONLY variables used in this step)
|
||||||
prdFile: '{prd_file_path}'
|
prdFile: '{prd_file_path}'
|
||||||
validationWorkflow: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-01-discovery.md'
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Step E-4: Complete & Validate
|
# Step E-4: Complete & Validate
|
||||||
|
|
@ -117,8 +116,7 @@ Display:
|
||||||
- Display: "This will run all 13 validation checks on the updated PRD."
|
- Display: "This will run all 13 validation checks on the updated PRD."
|
||||||
- Display: "Preparing to validate: {prd_file_path}"
|
- Display: "Preparing to validate: {prd_file_path}"
|
||||||
- Display: "**Proceeding to validation...**"
|
- Display: "**Proceeding to validation...**"
|
||||||
- Read fully and follow: {validationWorkflow} (steps-v/step-v-01-discovery.md)
|
- Invoke the `bmad-validate-prd` skill to run the complete validation workflow
|
||||||
- Note: This hands off to the validation workflow which will run its complete 13-step process
|
|
||||||
|
|
||||||
- **IF E (Edit More):**
|
- **IF E (Edit More):**
|
||||||
- Display: "**Additional Edits**"
|
- Display: "**Additional Edits**"
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ Before listing artifacts or prompting the user, check whether you already know t
|
||||||
|
|
||||||
1. Explicit argument
|
1. Explicit argument
|
||||||
Did the user pass a specific file path, spec name, or clear instruction this message?
|
Did the user pass a specific file path, spec name, or clear instruction this message?
|
||||||
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, or in-review) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review).
|
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review). For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
|
||||||
- Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
|
- Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
|
||||||
|
|
||||||
2. Recent conversation
|
2. Recent conversation
|
||||||
|
|
@ -64,7 +64,7 @@ Never ask extra questions if you already understand what the user intends.
|
||||||
- On **K**: Proceed as-is.
|
- On **K**: Proceed as-is.
|
||||||
5. Route — choose exactly one:
|
5. Route — choose exactly one:
|
||||||
|
|
||||||
Derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists, append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
|
Derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists: if its status is `draft`, treat it as the same work and resume it (set `spec_file` to that path, **EARLY EXIT** → `./step-02-plan.md`); otherwise append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
|
||||||
|
|
||||||
**a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions.
|
**a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,12 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||||
|
|
||||||
## INSTRUCTIONS
|
## INSTRUCTIONS
|
||||||
|
|
||||||
1. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
|
1. Draft resume check. If `{spec_file}` exists with `status: draft`, read it and capture the verbatim `<frozen-after-approval>...</frozen-after-approval>` block as `preserved_intent`. Otherwise `preserved_intent` is empty.
|
||||||
2. Read `./spec-template.md` fully. Fill it out based on the intent and investigation, and write the result to `{spec_file}`.
|
2. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
|
||||||
3. Self-review against READY FOR DEVELOPMENT standard.
|
3. Read `./spec-template.md` fully. Fill it out based on the intent and investigation. If `{preserved_intent}` is non-empty, substitute it for the `<frozen-after-approval>` block in your filled spec before writing. Write the result to `{spec_file}`.
|
||||||
4. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
|
4. Self-review against READY FOR DEVELOPMENT standard.
|
||||||
5. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
|
5. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
|
||||||
|
6. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
|
||||||
- Show user the token count.
|
- Show user the token count.
|
||||||
- HALT and ask human: `[S] Split — carve off secondary goals` | `[K] Keep full spec — accept the risks`
|
- HALT and ask human: `[S] Split — carve off secondary goals` | `[K] Keep full spec — accept the risks`
|
||||||
- On **S**: Propose the split — name each secondary goal. Append deferred goals to `{deferred_work_file}`. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint.
|
- On **S**: Propose the split — name each secondary goal. Append deferred goals to `{deferred_work_file}`. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||||
spec_file: '' # set by step-01 before entering this step
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Step One-Shot: Implement, Review, Present
|
# Step One-Shot: Implement, Review, Present
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
/**
|
|
||||||
* install_to_bmad Flag — Design Contract Tests
|
|
||||||
*
|
|
||||||
* Unit tests against the functions that implement the install_to_bmad flag.
|
|
||||||
* These nail down the 4 core design decisions:
|
|
||||||
*
|
|
||||||
* 1. true/omitted → skill stays in _bmad/ (default behavior)
|
|
||||||
* 2. false → skill removed from _bmad/ after IDE install
|
|
||||||
* 3. No platform → no cleanup runs (cleanup lives in installVerbatimSkills)
|
|
||||||
* 4. Mixed flags → each skill evaluated independently
|
|
||||||
*
|
|
||||||
* Usage: node test/test-install-to-bmad.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require('node:path');
|
|
||||||
const os = require('node:os');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { loadSkillManifest, getInstallToBmad } = require('../tools/installer/ide/shared/skill-manifest');
|
|
||||||
|
|
||||||
// ANSI colors
|
|
||||||
const colors = {
|
|
||||||
reset: '\u001B[0m',
|
|
||||||
green: '\u001B[32m',
|
|
||||||
red: '\u001B[31m',
|
|
||||||
yellow: '\u001B[33m',
|
|
||||||
cyan: '\u001B[36m',
|
|
||||||
dim: '\u001B[2m',
|
|
||||||
};
|
|
||||||
|
|
||||||
let passed = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
function assert(condition, testName, errorMessage = '') {
|
|
||||||
if (condition) {
|
|
||||||
console.log(`${colors.green}✓${colors.reset} ${testName}`);
|
|
||||||
passed++;
|
|
||||||
} else {
|
|
||||||
console.log(`${colors.red}✗${colors.reset} ${testName}`);
|
|
||||||
if (errorMessage) {
|
|
||||||
console.log(` ${colors.dim}${errorMessage}${colors.reset}`);
|
|
||||||
}
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runTests() {
|
|
||||||
console.log(`${colors.cyan}========================================`);
|
|
||||||
console.log('install_to_bmad — Design Contract Tests');
|
|
||||||
console.log(`========================================${colors.reset}\n`);
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 1. true/omitted → getInstallToBmad returns true (keep in _bmad/)
|
|
||||||
// ============================================================
|
|
||||||
console.log(`${colors.yellow}Design decision 1: true or omitted → skill stays in _bmad/${colors.reset}\n`);
|
|
||||||
|
|
||||||
// Null manifest (no bmad-skill-manifest.yaml) → true
|
|
||||||
assert(getInstallToBmad(null, 'workflow.md') === true, 'null manifest defaults to true');
|
|
||||||
|
|
||||||
// Single-entry, flag omitted → true
|
|
||||||
assert(
|
|
||||||
getInstallToBmad({ __single: { type: 'skill' } }, 'workflow.md') === true,
|
|
||||||
'single-entry manifest with flag omitted defaults to true',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Single-entry, explicit true → true
|
|
||||||
assert(
|
|
||||||
getInstallToBmad({ __single: { type: 'skill', install_to_bmad: true } }, 'workflow.md') === true,
|
|
||||||
'single-entry manifest with explicit true returns true',
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 2. false → getInstallToBmad returns false (remove from _bmad/)
|
|
||||||
// ============================================================
|
|
||||||
console.log(`${colors.yellow}Design decision 2: false → skill removed from _bmad/${colors.reset}\n`);
|
|
||||||
|
|
||||||
// Single-entry, explicit false → false
|
|
||||||
assert(
|
|
||||||
getInstallToBmad({ __single: { type: 'skill', install_to_bmad: false } }, 'workflow.md') === false,
|
|
||||||
'single-entry manifest with explicit false returns false',
|
|
||||||
);
|
|
||||||
|
|
||||||
// loadSkillManifest round-trip: YAML with false is preserved through load
|
|
||||||
{
|
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-itb-'));
|
|
||||||
await fs.writeFile(path.join(tmpDir, 'bmad-skill-manifest.yaml'), 'type: skill\ninstall_to_bmad: false\n');
|
|
||||||
const loaded = await loadSkillManifest(tmpDir);
|
|
||||||
assert(getInstallToBmad(loaded, 'workflow.md') === false, 'loadSkillManifest preserves install_to_bmad: false through round-trip');
|
|
||||||
await fs.remove(tmpDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 3. No platform → cleanup only runs inside installVerbatimSkills
|
|
||||||
// (This is a design invariant: getInstallToBmad is only consulted
|
|
||||||
// during IDE install. Without a platform, the flag has no effect.)
|
|
||||||
// ============================================================
|
|
||||||
console.log(`${colors.yellow}Design decision 3: flag is a per-skill property, not a pipeline gate${colors.reset}\n`);
|
|
||||||
|
|
||||||
// The flag value is stored but doesn't trigger any side effects by itself.
|
|
||||||
// Cleanup is driven by reading the CSV column inside installVerbatimSkills.
|
|
||||||
// We verify the flag is just data — getInstallToBmad doesn't touch the filesystem.
|
|
||||||
{
|
|
||||||
const manifest = { __single: { type: 'skill', install_to_bmad: false } };
|
|
||||||
const result = getInstallToBmad(manifest, 'workflow.md');
|
|
||||||
assert(typeof result === 'boolean', 'getInstallToBmad returns a boolean (pure data, no side effects)');
|
|
||||||
assert(result === false, 'false value is faithfully returned for consumer to act on');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 4. Mixed flags → each skill evaluated independently
|
|
||||||
// ============================================================
|
|
||||||
console.log(`${colors.yellow}Design decision 4: mixed flags — each skill independent${colors.reset}\n`);
|
|
||||||
|
|
||||||
// Multi-entry manifest: different files can have different flags
|
|
||||||
{
|
|
||||||
const manifest = {
|
|
||||||
'workflow.md': { type: 'skill', install_to_bmad: false },
|
|
||||||
'other.md': { type: 'skill', install_to_bmad: true },
|
|
||||||
};
|
|
||||||
assert(getInstallToBmad(manifest, 'workflow.md') === false, 'multi-entry: workflow.md with false returns false');
|
|
||||||
assert(getInstallToBmad(manifest, 'other.md') === true, 'multi-entry: other.md with true returns true');
|
|
||||||
assert(getInstallToBmad(manifest, 'unknown.md') === true, 'multi-entry: unknown file defaults to true');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Summary
|
|
||||||
// ============================================================
|
|
||||||
console.log(`${colors.cyan}========================================`);
|
|
||||||
console.log('Results:');
|
|
||||||
console.log(` Passed: ${colors.green}${passed}${colors.reset}`);
|
|
||||||
console.log(` Failed: ${colors.red}${failed}${colors.reset}`);
|
|
||||||
console.log(`========================================${colors.reset}\n`);
|
|
||||||
|
|
||||||
if (failed === 0) {
|
|
||||||
console.log(`${colors.green}All install_to_bmad contract tests passed!${colors.reset}\n`);
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
console.log(`${colors.red}Some install_to_bmad contract tests failed${colors.reset}\n`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runTests().catch((error) => {
|
|
||||||
console.error(`${colors.red}Test runner failed:${colors.reset}`, error.message);
|
|
||||||
console.error(error.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
@ -59,8 +59,8 @@ async function createTestBmadFixture() {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(fixtureDir, '_config', 'skill-manifest.csv'),
|
path.join(fixtureDir, '_config', 'skill-manifest.csv'),
|
||||||
[
|
[
|
||||||
'canonicalId,name,description,module,path,install_to_bmad',
|
'canonicalId,name,description,module,path',
|
||||||
'"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md","true"',
|
'"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md"',
|
||||||
'',
|
'',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
);
|
);
|
||||||
|
|
@ -103,8 +103,8 @@ async function createSkillCollisionFixture() {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(configDir, 'skill-manifest.csv'),
|
path.join(configDir, 'skill-manifest.csv'),
|
||||||
[
|
[
|
||||||
'canonicalId,name,description,module,path,install_to_bmad',
|
'canonicalId,name,description,module,path',
|
||||||
'"bmad-help","bmad-help","Native help skill","core","_bmad/core/tasks/bmad-help/SKILL.md","true"',
|
'"bmad-help","bmad-help","Native help skill","core","_bmad/core/tasks/bmad-help/SKILL.md"',
|
||||||
'',
|
'',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
);
|
);
|
||||||
|
|
@ -1301,6 +1301,14 @@ async function runTests() {
|
||||||
'---\nname: bmad-architect\ndescription: Architect\n---\nOld skill content\n',
|
'---\nname: bmad-architect\ndescription: Architect\n---\nOld skill content\n',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add bmad-architect to the existing skill-manifest.csv so cleanup knows it was previously installed
|
||||||
|
const configDir27 = path.join(installedBmadDir27, '_config');
|
||||||
|
const existingCsv27 = await fs.readFile(path.join(configDir27, 'skill-manifest.csv'), 'utf8');
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(configDir27, 'skill-manifest.csv'),
|
||||||
|
existingCsv27.trimEnd() + '\n"bmad-architect","bmad-architect","Architect","bmm","_bmad/bmm/agents/bmad-architect/SKILL.md"\n',
|
||||||
|
);
|
||||||
|
|
||||||
// Run Claude Code setup (which triggers cleanup then install)
|
// Run Claude Code setup (which triggers cleanup then install)
|
||||||
const ideManager27 = new IdeManager();
|
const ideManager27 = new IdeManager();
|
||||||
await ideManager27.ensureInitialized();
|
await ideManager27.ensureInitialized();
|
||||||
|
|
|
||||||
|
|
@ -19,24 +19,33 @@ const CLIUtils = {
|
||||||
* Display BMAD logo and version using @clack intro + box
|
* Display BMAD logo and version using @clack intro + box
|
||||||
*/
|
*/
|
||||||
async displayLogo() {
|
async displayLogo() {
|
||||||
const version = this.getVersion();
|
|
||||||
const color = await prompts.getColor();
|
const color = await prompts.getColor();
|
||||||
|
const termWidth = process.stdout.columns || 80;
|
||||||
|
|
||||||
// ASCII art logo
|
// Full "BMad Method" logo for wide terminals, "BMad" only for narrow
|
||||||
const logo = [
|
const logoWide = [
|
||||||
|
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ™',
|
||||||
|
'██╔══██╗████╗ ████║██╔══██╗██╔══██╗ ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗',
|
||||||
|
'██████╔╝██╔████╔██║███████║██║ ██║ ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║',
|
||||||
|
'██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║ ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║',
|
||||||
|
'██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝',
|
||||||
|
'╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ',
|
||||||
|
];
|
||||||
|
|
||||||
|
const logoNarrow = [
|
||||||
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
|
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
|
||||||
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
|
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
|
||||||
' ██████╔╝██╔████╔██║███████║██║ ██║',
|
' ██████╔╝██╔████╔██║███████║██║ ██║',
|
||||||
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
|
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
|
||||||
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
|
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
|
||||||
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
|
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
|
||||||
]
|
];
|
||||||
.map((line) => color.yellow(line))
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const tagline = ' Build More, Architect Dreams';
|
const logoLines = termWidth >= 95 ? logoWide : logoNarrow;
|
||||||
|
const logo = logoLines.map((line) => color.blue(line)).join('\n');
|
||||||
|
const tagline = color.white(' Build More, Architect Dreams\n © BMad Code');
|
||||||
|
|
||||||
await prompts.box(`${logo}\n${tagline}`, `v${version}`, {
|
await prompts.box(`${logo}\n${tagline}`, '', {
|
||||||
contentAlign: 'center',
|
contentAlign: 'center',
|
||||||
rounded: true,
|
rounded: true,
|
||||||
formatBorder: color.blue,
|
formatBorder: color.blue,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,44 @@ class Installer {
|
||||||
this.bmadFolderName = BMAD_FOLDER_NAME;
|
this.bmadFolderName = BMAD_FOLDER_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the module version from .claude-plugin/marketplace.json
|
||||||
|
* Walks up from sourcePath looking for .claude-plugin/marketplace.json
|
||||||
|
* @param {string} sourcePath - Module source directory
|
||||||
|
* @returns {string} Version string or empty string
|
||||||
|
*/
|
||||||
|
async _getMarketplaceVersion(sourcePath) {
|
||||||
|
let dir = sourcePath;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const marketplacePath = path.join(dir, '.claude-plugin', 'marketplace.json');
|
||||||
|
if (await fs.pathExists(marketplacePath)) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||||
|
return this._extractMarketplaceVersion(data);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = path.dirname(dir);
|
||||||
|
if (parent === dir) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the highest version from marketplace.json plugins array
|
||||||
|
*/
|
||||||
|
_extractMarketplaceVersion(data) {
|
||||||
|
const plugins = data?.plugins;
|
||||||
|
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
||||||
|
let best = '';
|
||||||
|
for (const p of plugins) {
|
||||||
|
if (p.version && (!best || p.version > best)) best = p.version;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main installation method
|
* Main installation method
|
||||||
* @param {Object} config - Installation configuration
|
* @param {Object} config - Installation configuration
|
||||||
|
|
@ -52,9 +90,36 @@ class Installer {
|
||||||
|
|
||||||
await this._validateIdeSelection(config);
|
await this._validateIdeSelection(config);
|
||||||
|
|
||||||
|
// Capture pre-install module versions for from→to display
|
||||||
|
const preInstallVersions = new Map();
|
||||||
|
if (existingInstall.installed) {
|
||||||
|
const existingModules = await this.manifest.getAllModuleVersions(paths.bmadDir);
|
||||||
|
for (const mod of existingModules) {
|
||||||
|
if (mod.name && mod.version) {
|
||||||
|
preInstallVersions.set(mod.name, mod.version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Results collector for consolidated summary
|
// Results collector for consolidated summary
|
||||||
const results = [];
|
const results = [];
|
||||||
const addResult = (step, status, detail = '') => results.push({ step, status, detail });
|
const addResult = (step, status, detail = '', meta = {}) => results.push({ step, status, detail, ...meta });
|
||||||
|
|
||||||
|
// Capture previously installed skill IDs before they get overwritten
|
||||||
|
const previousSkillIds = new Set();
|
||||||
|
const prevCsvPath = path.join(paths.bmadDir, '_config', 'skill-manifest.csv');
|
||||||
|
if (await fs.pathExists(prevCsvPath)) {
|
||||||
|
try {
|
||||||
|
const csvParse = require('csv-parse/sync');
|
||||||
|
const content = await fs.readFile(prevCsvPath, 'utf8');
|
||||||
|
const records = csvParse.parse(content, { columns: true, skip_empty_lines: true });
|
||||||
|
for (const r of records) {
|
||||||
|
if (r.canonicalId) previousSkillIds.add(r.canonicalId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this._cacheCustomModules(paths, addResult);
|
await this._cacheCustomModules(paths, addResult);
|
||||||
|
|
||||||
|
|
@ -65,7 +130,11 @@ class Installer {
|
||||||
|
|
||||||
await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules);
|
await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules);
|
||||||
|
|
||||||
await this._setupIdes(config, allModules, paths, addResult);
|
await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
|
||||||
|
|
||||||
|
// Skills are now in IDE directories — remove redundant copies from _bmad/.
|
||||||
|
// Also cleans up skill dirs left by older installer versions.
|
||||||
|
await this._cleanupSkillDirs(paths.bmadDir);
|
||||||
|
|
||||||
const restoreResult = await this._restoreUserFiles(paths, updateState);
|
const restoreResult = await this._restoreUserFiles(paths, updateState);
|
||||||
|
|
||||||
|
|
@ -76,6 +145,7 @@ class Installer {
|
||||||
ides: config.ides,
|
ides: config.ides,
|
||||||
customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
|
customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
|
||||||
modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
|
modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
|
||||||
|
preInstallVersions,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -321,7 +391,7 @@ class Installer {
|
||||||
/**
|
/**
|
||||||
* Set up IDE integrations for each selected IDE.
|
* Set up IDE integrations for each selected IDE.
|
||||||
*/
|
*/
|
||||||
async _setupIdes(config, allModules, paths, addResult) {
|
async _setupIdes(config, allModules, paths, addResult, previousSkillIds = new Set()) {
|
||||||
if (config.skipIde || !config.ides || config.ides.length === 0) return;
|
if (config.skipIde || !config.ides || config.ides.length === 0) return;
|
||||||
|
|
||||||
await this.ideManager.ensureInitialized();
|
await this.ideManager.ensureInitialized();
|
||||||
|
|
@ -336,6 +406,7 @@ class Installer {
|
||||||
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
|
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
|
||||||
selectedModules: allModules || [],
|
selectedModules: allModules || [],
|
||||||
verbose: config.verbose,
|
verbose: config.verbose,
|
||||||
|
previousSkillIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (setupResult.success) {
|
if (setupResult.success) {
|
||||||
|
|
@ -346,6 +417,33 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove skill directories from _bmad/ after IDE installation.
|
||||||
|
* Skills are self-contained in IDE directories, so _bmad/ only needs
|
||||||
|
* module-level files (config.yaml, _config/, etc.).
|
||||||
|
* Also cleans up skill dirs left by older installer versions.
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
*/
|
||||||
|
async _cleanupSkillDirs(bmadDir) {
|
||||||
|
const csv = require('csv-parse/sync');
|
||||||
|
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
||||||
|
if (!(await fs.pathExists(csvPath))) return;
|
||||||
|
|
||||||
|
const csvContent = await fs.readFile(csvPath, 'utf8');
|
||||||
|
const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true });
|
||||||
|
const bmadFolderName = path.basename(bmadDir);
|
||||||
|
const bmadPrefix = bmadFolderName + '/';
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
if (!record.path) continue;
|
||||||
|
const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;
|
||||||
|
const sourceDir = path.dirname(path.join(bmadDir, relativePath));
|
||||||
|
if (await fs.pathExists(sourceDir)) {
|
||||||
|
await fs.remove(sourceDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore custom and modified files that were backed up before the update.
|
* Restore custom and modified files that were backed up before the update.
|
||||||
* No-op for fresh installs (updateState is null).
|
* No-op for fresh installs (updateState is null).
|
||||||
|
|
@ -556,7 +654,7 @@ class Installer {
|
||||||
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
||||||
|
|
||||||
const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
|
const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
|
||||||
await officialModules.install(
|
const installResult = await officialModules.install(
|
||||||
moduleName,
|
moduleName,
|
||||||
paths.bmadDir,
|
paths.bmadDir,
|
||||||
(filePath) => {
|
(filePath) => {
|
||||||
|
|
@ -570,7 +668,12 @@ class Installer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
|
// Get display name from source module.yaml; version from marketplace.json
|
||||||
|
const sourcePath = await officialModules.findModuleSource(moduleName, { silent: true });
|
||||||
|
const moduleInfo = sourcePath ? await officialModules.getModuleInfo(sourcePath, moduleName, '') : null;
|
||||||
|
const displayName = moduleInfo?.name || moduleName;
|
||||||
|
const version = sourcePath ? await this._getMarketplaceVersion(sourcePath) : '';
|
||||||
|
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -598,7 +701,11 @@ class Installer {
|
||||||
[moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
|
[moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
|
||||||
});
|
});
|
||||||
|
|
||||||
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
|
// Get display name from source module.yaml; version from marketplace.json
|
||||||
|
const moduleInfo = await officialModules.getModuleInfo(sourcePath, moduleName, '');
|
||||||
|
const displayName = moduleInfo?.name || moduleName;
|
||||||
|
const version = await this._getMarketplaceVersion(sourcePath);
|
||||||
|
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1062,23 +1169,10 @@ class Installer {
|
||||||
const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
|
const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
|
||||||
|
|
||||||
// Build step lines with status indicators
|
// Build step lines with status indicators
|
||||||
|
const preVersions = context.preInstallVersions || new Map();
|
||||||
const lines = [];
|
const lines = [];
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
let stepLabel = null;
|
const stepLabel = r.step;
|
||||||
|
|
||||||
if (r.status !== 'ok') {
|
|
||||||
stepLabel = r.step;
|
|
||||||
} else if (r.step === 'Core') {
|
|
||||||
stepLabel = 'BMAD';
|
|
||||||
} else if (r.step.startsWith('Module: ')) {
|
|
||||||
stepLabel = r.step;
|
|
||||||
} else if (selectedIdes.has(String(r.step).toLowerCase())) {
|
|
||||||
stepLabel = r.step;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stepLabel) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon;
|
let icon;
|
||||||
if (r.status === 'ok') {
|
if (r.status === 'ok') {
|
||||||
|
|
@ -1088,18 +1182,32 @@ class Installer {
|
||||||
} else {
|
} else {
|
||||||
icon = color.red('\u2717');
|
icon = color.red('\u2717');
|
||||||
}
|
}
|
||||||
const detail = r.detail ? color.dim(` (${r.detail})`) : '';
|
|
||||||
|
// Build version detail for module results
|
||||||
|
let detail = '';
|
||||||
|
if (r.moduleCode && r.newVersion) {
|
||||||
|
const oldVersion = preVersions.get(r.moduleCode);
|
||||||
|
if (oldVersion && oldVersion === r.newVersion) {
|
||||||
|
detail = ` (v${r.newVersion}, no change)`;
|
||||||
|
} else if (oldVersion) {
|
||||||
|
detail = ` (v${oldVersion} → v${r.newVersion})`;
|
||||||
|
} else {
|
||||||
|
detail = ` (v${r.newVersion}, installed)`;
|
||||||
|
}
|
||||||
|
} else if (r.detail) {
|
||||||
|
detail = ` (${r.detail})`;
|
||||||
|
}
|
||||||
lines.push(` ${icon} ${stepLabel}${detail}`);
|
lines.push(` ${icon} ${stepLabel}${detail}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((context.ides || []).length === 0) {
|
if ((context.ides || []).length === 0) {
|
||||||
lines.push(` ${color.green('\u2713')} No IDE selected ${color.dim('(installed in _bmad only)')}`);
|
lines.push(` ${color.green('\u2713')} No IDE selected (installed in _bmad only)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context and warnings
|
// Context and warnings
|
||||||
lines.push('');
|
lines.push('');
|
||||||
if (context.bmadDir) {
|
if (context.bmadDir) {
|
||||||
lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
|
lines.push(` Installed to: ${context.bmadDir}`);
|
||||||
}
|
}
|
||||||
if (context.customFiles && context.customFiles.length > 0) {
|
if (context.customFiles && context.customFiles.length > 0) {
|
||||||
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
|
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
|
||||||
|
|
@ -1111,17 +1219,18 @@ class Installer {
|
||||||
// Next steps
|
// Next steps
|
||||||
lines.push(
|
lines.push(
|
||||||
'',
|
'',
|
||||||
' Next steps:',
|
' Get started:',
|
||||||
` Read our new Docs Site: ${color.dim('https://docs.bmad-method.org/')}`,
|
` 1. Launch your AI agent from your project folder`,
|
||||||
` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
|
` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
|
||||||
` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`,
|
'',
|
||||||
` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`,
|
` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
|
||||||
|
` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
|
||||||
);
|
);
|
||||||
if (context.ides && context.ides.length > 0) {
|
|
||||||
lines.push(` Invoke the ${color.cyan('bmad-help')} skill in your IDE Agent to get started`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
|
await prompts.box(lines.join('\n'), 'BMAD is ready to use!', {
|
||||||
|
rounded: true,
|
||||||
|
formatBorder: color.green,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1231,6 +1340,7 @@ class Installer {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const moduleName of modulesToUpdate) {
|
for (const moduleName of modulesToUpdate) {
|
||||||
|
if (moduleName === 'core') continue; // Already collected above
|
||||||
const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
|
const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
|
||||||
if (modulePrompted) {
|
if (modulePrompted) {
|
||||||
promptedForNewFields = true;
|
promptedForNewFields = true;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ const {
|
||||||
loadSkillManifest: loadSkillManifestShared,
|
loadSkillManifest: loadSkillManifestShared,
|
||||||
getCanonicalId: getCanonicalIdShared,
|
getCanonicalId: getCanonicalIdShared,
|
||||||
getArtifactType: getArtifactTypeShared,
|
getArtifactType: getArtifactTypeShared,
|
||||||
getInstallToBmad: getInstallToBmadShared,
|
|
||||||
} = require('../ide/shared/skill-manifest');
|
} = require('../ide/shared/skill-manifest');
|
||||||
|
|
||||||
// Load package.json for version info
|
// Load package.json for version info
|
||||||
|
|
@ -42,11 +41,6 @@ class ManifestGenerator {
|
||||||
return getArtifactTypeShared(manifest, filename);
|
return getArtifactTypeShared(manifest, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Delegate to shared skill-manifest module */
|
|
||||||
getInstallToBmad(manifest, filename) {
|
|
||||||
return getInstallToBmadShared(manifest, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean text for CSV output by normalizing whitespace.
|
* Clean text for CSV output by normalizing whitespace.
|
||||||
* Note: Quote escaping is handled by escapeCsv() at write time.
|
* Note: Quote escaping is handled by escapeCsv() at write time.
|
||||||
|
|
@ -127,7 +121,7 @@ class ManifestGenerator {
|
||||||
* Recursively walk a module directory tree, collecting native SKILL.md entrypoints.
|
* Recursively walk a module directory tree, collecting native SKILL.md entrypoints.
|
||||||
* A directory is discovered as a skill when it contains a SKILL.md file with
|
* A directory is discovered as a skill when it contains a SKILL.md file with
|
||||||
* valid name/description frontmatter (name must match directory name).
|
* valid name/description frontmatter (name must match directory name).
|
||||||
* Manifest YAML is loaded only when present — for install_to_bmad and agent metadata.
|
* Manifest YAML is loaded only when present — for agent metadata.
|
||||||
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
|
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
|
||||||
*/
|
*/
|
||||||
async collectSkills() {
|
async collectSkills() {
|
||||||
|
|
@ -156,7 +150,7 @@ class ManifestGenerator {
|
||||||
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
|
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
|
||||||
|
|
||||||
if (skillMeta) {
|
if (skillMeta) {
|
||||||
// Load manifest when present (for install_to_bmad and agent metadata)
|
// Load manifest when present (for agent metadata)
|
||||||
const manifest = await this.loadSkillManifest(dir);
|
const manifest = await this.loadSkillManifest(dir);
|
||||||
const artifactType = this.getArtifactType(manifest, skillFile);
|
const artifactType = this.getArtifactType(manifest, skillFile);
|
||||||
|
|
||||||
|
|
@ -182,7 +176,6 @@ class ManifestGenerator {
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
canonicalId,
|
canonicalId,
|
||||||
install_to_bmad: this.getInstallToBmad(manifest, skillFile),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to files list
|
// Add to files list
|
||||||
|
|
@ -472,7 +465,7 @@ class ManifestGenerator {
|
||||||
const csvPath = path.join(cfgDir, 'skill-manifest.csv');
|
const csvPath = path.join(cfgDir, 'skill-manifest.csv');
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||||
|
|
||||||
let csvContent = 'canonicalId,name,description,module,path,install_to_bmad\n';
|
let csvContent = 'canonicalId,name,description,module,path\n';
|
||||||
|
|
||||||
for (const skill of this.skills) {
|
for (const skill of this.skills) {
|
||||||
const row = [
|
const row = [
|
||||||
|
|
@ -481,7 +474,6 @@ class ManifestGenerator {
|
||||||
escapeCsv(skill.description),
|
escapeCsv(skill.description),
|
||||||
escapeCsv(skill.module),
|
escapeCsv(skill.module),
|
||||||
escapeCsv(skill.path),
|
escapeCsv(skill.path),
|
||||||
escapeCsv(skill.install_to_bmad),
|
|
||||||
].join(',');
|
].join(',');
|
||||||
csvContent += row + '\n';
|
csvContent += row + '\n';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -837,14 +837,13 @@ class Manifest {
|
||||||
* @returns {Object} Version info object with version, source, npmPackage, repoUrl
|
* @returns {Object} Version info object with version, source, npmPackage, repoUrl
|
||||||
*/
|
*/
|
||||||
async getModuleVersionInfo(moduleName, bmadDir, moduleSourcePath = null) {
|
async getModuleVersionInfo(moduleName, bmadDir, moduleSourcePath = null) {
|
||||||
const os = require('node:os');
|
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
|
|
||||||
// Built-in modules use BMad version (only core and bmm are in BMAD-METHOD repo)
|
// Resolve source type first, then read version with the correct path context
|
||||||
if (['core', 'bmm'].includes(moduleName)) {
|
if (['core', 'bmm'].includes(moduleName)) {
|
||||||
const bmadVersion = require(path.join(getProjectRoot(), 'package.json')).version;
|
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
||||||
return {
|
return {
|
||||||
version: bmadVersion,
|
version,
|
||||||
source: 'built-in',
|
source: 'built-in',
|
||||||
npmPackage: null,
|
npmPackage: null,
|
||||||
repoUrl: null,
|
repoUrl: null,
|
||||||
|
|
@ -857,42 +856,20 @@ class Manifest {
|
||||||
const moduleInfo = await extMgr.getModuleByCode(moduleName);
|
const moduleInfo = await extMgr.getModuleByCode(moduleName);
|
||||||
|
|
||||||
if (moduleInfo) {
|
if (moduleInfo) {
|
||||||
// External module - try to get version from npm registry first, then fall back to cache
|
// External module: use moduleSourcePath if provided, otherwise fall back to cache
|
||||||
let version = null;
|
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
||||||
|
|
||||||
if (moduleInfo.npmPackage) {
|
|
||||||
// Fetch version from npm registry
|
|
||||||
try {
|
|
||||||
version = await this.fetchNpmVersion(moduleInfo.npmPackage);
|
|
||||||
} catch {
|
|
||||||
// npm fetch failed, try cache as fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If npm didn't work, try reading from cached repo's package.json
|
|
||||||
if (!version) {
|
|
||||||
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
|
|
||||||
const packageJsonPath = path.join(cacheDir, 'package.json');
|
|
||||||
|
|
||||||
if (await fs.pathExists(packageJsonPath)) {
|
|
||||||
try {
|
|
||||||
const pkg = require(packageJsonPath);
|
|
||||||
version = pkg.version;
|
|
||||||
} catch (error) {
|
|
||||||
await prompts.log.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: version,
|
version,
|
||||||
source: 'external',
|
source: 'external',
|
||||||
npmPackage: moduleInfo.npmPackage || null,
|
npmPackage: moduleInfo.npmPackage || null,
|
||||||
repoUrl: moduleInfo.url || null,
|
repoUrl: moduleInfo.url || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom module - check cache directory
|
// Custom module: resolve path from source or cache before reading version
|
||||||
|
const customSourcePath = moduleSourcePath || path.join(bmadDir, '_config', 'custom', moduleName);
|
||||||
|
const version = await this._readMarketplaceVersion(moduleName, customSourcePath);
|
||||||
|
|
||||||
const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName);
|
const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName);
|
||||||
const moduleYamlPath = path.join(cacheDir, 'module.yaml');
|
const moduleYamlPath = path.join(cacheDir, 'module.yaml');
|
||||||
|
|
||||||
|
|
@ -901,7 +878,7 @@ class Manifest {
|
||||||
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
||||||
const moduleConfig = yaml.parse(yamlContent);
|
const moduleConfig = yaml.parse(yamlContent);
|
||||||
return {
|
return {
|
||||||
version: moduleConfig.version || null,
|
version: version || moduleConfig.version || null,
|
||||||
source: 'custom',
|
source: 'custom',
|
||||||
npmPackage: moduleConfig.npmPackage || null,
|
npmPackage: moduleConfig.npmPackage || null,
|
||||||
repoUrl: moduleConfig.repoUrl || null,
|
repoUrl: moduleConfig.repoUrl || null,
|
||||||
|
|
@ -913,13 +890,62 @@ class Manifest {
|
||||||
|
|
||||||
// Unknown module
|
// Unknown module
|
||||||
return {
|
return {
|
||||||
version: null,
|
version,
|
||||||
source: 'unknown',
|
source: 'unknown',
|
||||||
npmPackage: null,
|
npmPackage: null,
|
||||||
repoUrl: null,
|
repoUrl: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read version from .claude-plugin/marketplace.json for a module
|
||||||
|
* @param {string} moduleName - Module code
|
||||||
|
* @returns {string|null} Version or null
|
||||||
|
*/
|
||||||
|
async _readMarketplaceVersion(moduleName, moduleSourcePath = null) {
|
||||||
|
const os = require('node:os');
|
||||||
|
let marketplacePath;
|
||||||
|
|
||||||
|
if (['core', 'bmm'].includes(moduleName)) {
|
||||||
|
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
||||||
|
} else if (moduleSourcePath) {
|
||||||
|
// Walk up from source path to find marketplace.json
|
||||||
|
let dir = moduleSourcePath;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const candidate = path.join(dir, '.claude-plugin', 'marketplace.json');
|
||||||
|
if (await fs.pathExists(candidate)) {
|
||||||
|
marketplacePath = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const parent = path.dirname(dir);
|
||||||
|
if (parent === dir) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to external module cache
|
||||||
|
if (!marketplacePath) {
|
||||||
|
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
|
||||||
|
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await fs.pathExists(marketplacePath)) {
|
||||||
|
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||||
|
const plugins = data?.plugins;
|
||||||
|
if (!Array.isArray(plugins) || plugins.length === 0) return null;
|
||||||
|
let best = null;
|
||||||
|
for (const p of plugins) {
|
||||||
|
if (p.version && (!best || p.version > best)) best = p.version;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch latest version from npm for a package
|
* Fetch latest version from npm for a package
|
||||||
* @param {string} packageName - npm package name
|
* @param {string} packageName - npm package name
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ class ConfigDrivenIdeSetup {
|
||||||
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
|
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
|
||||||
|
|
||||||
// Clean up any old BMAD installation first
|
// Clean up any old BMAD installation first
|
||||||
await this.cleanup(projectDir, options);
|
await this.cleanup(projectDir, options, bmadDir);
|
||||||
|
|
||||||
if (!this.installerConfig) {
|
if (!this.installerConfig) {
|
||||||
return { success: false, reason: 'no-config' };
|
return { success: false, reason: 'no-config' };
|
||||||
|
|
@ -183,18 +183,6 @@ class ConfigDrivenIdeSetup {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post-install cleanup: remove _bmad/ directories for skills with install_to_bmad === "false"
|
|
||||||
for (const record of records) {
|
|
||||||
if (record.install_to_bmad === 'false') {
|
|
||||||
const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;
|
|
||||||
const sourceFile = path.join(bmadDir, relativePath);
|
|
||||||
const sourceDir = path.dirname(sourceFile);
|
|
||||||
if (await fs.pathExists(sourceDir)) {
|
|
||||||
await fs.remove(sourceDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,15 +203,34 @@ class ConfigDrivenIdeSetup {
|
||||||
* Cleanup IDE configuration
|
* Cleanup IDE configuration
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
*/
|
*/
|
||||||
async cleanup(projectDir, options = {}) {
|
async cleanup(projectDir, options = {}, bmadDir = null) {
|
||||||
|
const resolvedBmadDir = bmadDir || (await this._findBmadDir(projectDir));
|
||||||
|
|
||||||
|
// Build removal set: previously installed skills + removals.txt entries
|
||||||
|
let removalSet;
|
||||||
|
if (options.previousSkillIds && options.previousSkillIds.size > 0) {
|
||||||
|
// Install/update flow: use pre-captured skill IDs (before manifest was overwritten)
|
||||||
|
removalSet = new Set(options.previousSkillIds);
|
||||||
|
if (resolvedBmadDir) {
|
||||||
|
const removals = await this.loadRemovalLists(resolvedBmadDir);
|
||||||
|
for (const entry of removals) removalSet.add(entry);
|
||||||
|
}
|
||||||
|
} else if (resolvedBmadDir) {
|
||||||
|
// Uninstall flow: read from current skill-manifest.csv + removals.txt
|
||||||
|
removalSet = await this._buildUninstallSet(resolvedBmadDir);
|
||||||
|
} else {
|
||||||
|
removalSet = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
|
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
|
||||||
|
// Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
|
||||||
if (this.installerConfig?.legacy_targets) {
|
if (this.installerConfig?.legacy_targets) {
|
||||||
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
||||||
for (const legacyDir of this.installerConfig.legacy_targets) {
|
for (const legacyDir of this.installerConfig.legacy_targets) {
|
||||||
if (this.isGlobalPath(legacyDir)) {
|
if (this.isGlobalPath(legacyDir)) {
|
||||||
await this.warnGlobalLegacy(legacyDir, options);
|
await this.warnGlobalLegacy(legacyDir, options);
|
||||||
} else {
|
} else {
|
||||||
await this.cleanupTarget(projectDir, legacyDir, options);
|
await this.cleanupTarget(projectDir, legacyDir, options, null);
|
||||||
await this.removeEmptyParents(projectDir, legacyDir);
|
await this.removeEmptyParents(projectDir, legacyDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,9 +251,9 @@ class ConfigDrivenIdeSetup {
|
||||||
await this.cleanupRovoDevPrompts(projectDir, options);
|
await this.cleanupRovoDevPrompts(projectDir, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean target directory
|
// Clean current target directory
|
||||||
if (this.installerConfig?.target_dir) {
|
if (this.installerConfig?.target_dir) {
|
||||||
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options);
|
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,23 +293,117 @@ class ConfigDrivenIdeSetup {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup a specific target directory
|
* Find the _bmad directory in a project
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
* @returns {string|null} Path to bmad dir or null
|
||||||
|
*/
|
||||||
|
async _findBmadDir(projectDir) {
|
||||||
|
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
||||||
|
return (await fs.pathExists(bmadDir)) ? bmadDir : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the full set of entries to remove for uninstall.
|
||||||
|
* Reads skill-manifest.csv to know exactly what was installed, plus removal lists.
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @returns {Set<string>} Set of entries to remove
|
||||||
|
*/
|
||||||
|
async _buildUninstallSet(bmadDir) {
|
||||||
|
const removals = await this.loadRemovalLists(bmadDir);
|
||||||
|
|
||||||
|
// Also add all currently installed skills from skill-manifest.csv
|
||||||
|
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
||||||
|
try {
|
||||||
|
if (await fs.pathExists(csvPath)) {
|
||||||
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
|
const records = csv.parse(content, { columns: true, skip_empty_lines: true });
|
||||||
|
for (const record of records) {
|
||||||
|
if (record.canonicalId) {
|
||||||
|
removals.add(record.canonicalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If we can't read the manifest, we still have the removal lists
|
||||||
|
}
|
||||||
|
|
||||||
|
return removals;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load removal lists from all module sources in the bmad directory.
|
||||||
|
* Each module can have an optional removals.txt listing entries to remove.
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @returns {Set<string>} Set of entries to remove
|
||||||
|
*/
|
||||||
|
async loadRemovalLists(bmadDir) {
|
||||||
|
const removals = new Set();
|
||||||
|
const { getProjectRoot } = require('../project-root');
|
||||||
|
|
||||||
|
// Read project-level removals.txt (covers core and bmm)
|
||||||
|
const projectRemovalsPath = path.join(getProjectRoot(), 'removals.txt');
|
||||||
|
await this._readRemovalFile(projectRemovalsPath, removals);
|
||||||
|
|
||||||
|
// Read per-module removals.txt from installed module directories
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(bmadDir);
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.startsWith('_')) continue;
|
||||||
|
const removalPath = path.join(bmadDir, entry, 'removals.txt');
|
||||||
|
await this._readRemovalFile(removalPath, removals);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// bmadDir may not exist yet on fresh install
|
||||||
|
}
|
||||||
|
|
||||||
|
return removals;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a removals.txt file and add entries to the set
|
||||||
|
* @param {string} filePath - Path to removals.txt
|
||||||
|
* @param {Set<string>} removals - Set to add entries to
|
||||||
|
*/
|
||||||
|
async _readRemovalFile(filePath, removals) {
|
||||||
|
try {
|
||||||
|
if (await fs.pathExists(filePath)) {
|
||||||
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
for (const line of content.split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed && !trimmed.startsWith('#')) {
|
||||||
|
removals.add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Optional file — ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup a specific target directory.
|
||||||
|
* When removalSet is provided, only removes entries in that set.
|
||||||
|
* When removalSet is null (legacy dirs), removes all bmad-prefixed entries.
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
* @param {string} targetDir - Target directory to clean
|
* @param {string} targetDir - Target directory to clean
|
||||||
|
* @param {Object} options - Cleanup options
|
||||||
|
* @param {Set<string>|null} removalSet - Entries to remove, or null for legacy prefix matching
|
||||||
*/
|
*/
|
||||||
async cleanupTarget(projectDir, targetDir, options = {}) {
|
async cleanupTarget(projectDir, targetDir, options = {}, removalSet = new Set()) {
|
||||||
const targetPath = path.join(projectDir, targetDir);
|
const targetPath = path.join(projectDir, targetDir);
|
||||||
|
|
||||||
if (!(await fs.pathExists(targetPath))) {
|
if (!(await fs.pathExists(targetPath))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all bmad* files
|
if (removalSet && removalSet.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = await fs.readdir(targetPath);
|
entries = await fs.readdir(targetPath);
|
||||||
} catch {
|
} catch {
|
||||||
// Directory exists but can't be read - skip cleanup
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,23 +414,26 @@ class ConfigDrivenIdeSetup {
|
||||||
let removedCount = 0;
|
let removedCount = 0;
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry || typeof entry !== 'string') {
|
if (!entry || typeof entry !== 'string') continue;
|
||||||
continue;
|
|
||||||
}
|
// Always preserve bmad-os-* utility skills regardless of cleanup mode
|
||||||
if (entry.startsWith('bmad') && !entry.startsWith('bmad-os-')) {
|
if (entry.startsWith('bmad-os-')) continue;
|
||||||
const entryPath = path.join(targetPath, entry);
|
|
||||||
|
// Surgical removal from set, or legacy prefix matching when set is null
|
||||||
|
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
|
||||||
|
|
||||||
|
if (shouldRemove) {
|
||||||
try {
|
try {
|
||||||
await fs.remove(entryPath);
|
await fs.remove(path.join(targetPath, entry));
|
||||||
removedCount++;
|
removedCount++;
|
||||||
} catch {
|
} catch {
|
||||||
// Skip entries that can't be removed (broken symlinks, permission errors)
|
// Skip entries that can't be removed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removedCount > 0 && !options.silent) {
|
// Only log cleanup when it's not a routine reinstall (legacy dir cleanup or actual removals)
|
||||||
await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`);
|
// Suppress for current target_dir since it's always cleaned before a fresh write
|
||||||
}
|
|
||||||
|
|
||||||
// Remove empty directory after cleanup
|
// Remove empty directory after cleanup
|
||||||
if (removedCount > 0) {
|
if (removedCount > 0) {
|
||||||
|
|
@ -339,7 +443,7 @@ class ConfigDrivenIdeSetup {
|
||||||
await fs.remove(targetPath);
|
await fs.remove(targetPath);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Directory may already be gone or in use — skip
|
// Directory may already be gone or in use
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,19 +54,4 @@ function getArtifactType(manifest, filename) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
module.exports = { loadSkillManifest, getCanonicalId, getArtifactType };
|
||||||
* Get the install_to_bmad flag for a specific file from a loaded skill manifest.
|
|
||||||
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)
|
|
||||||
* @param {string} filename - Source filename to look up
|
|
||||||
* @returns {boolean} install_to_bmad value (defaults to true)
|
|
||||||
*/
|
|
||||||
function getInstallToBmad(manifest, filename) {
|
|
||||||
if (!manifest) return true;
|
|
||||||
// Single-entry manifest applies to all files in the directory
|
|
||||||
if (manifest.__single) return manifest.__single.install_to_bmad !== false;
|
|
||||||
// Multi-entry: look up by filename directly
|
|
||||||
if (manifest[filename]) return manifest[filename].install_to_bmad !== false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { loadSkillManifest, getCanonicalId, getArtifactType, getInstallToBmad };
|
|
||||||
|
|
|
||||||
|
|
@ -6,32 +6,25 @@
|
||||||
startMessage: |
|
startMessage: |
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
🎉 V6 IS HERE! Welcome to BMad Method V6 - Official Stable Release!
|
Agile AI-Driven Development. Powered by BMad Core and a growing module ecosystem.
|
||||||
|
Install official and community modules during setup to customize your experience.
|
||||||
|
|
||||||
The BMad Method is now a Platform powered by the BMad Method Core and Module Ecosystem!
|
🌟 100% free. 100% open source. Always.
|
||||||
- Select and install modules during setup - customize your experience
|
No paywalls. No gated content. Knowledge shared, not sold.
|
||||||
- New BMad Method for Agile AI-Driven Development (the evolution of V4)
|
|
||||||
- Exciting new modules available during installation, with community modules coming soon
|
|
||||||
- Documentation: https://docs.bmad-method.org
|
|
||||||
|
|
||||||
🌟 BMad is 100% free and open source.
|
🌐 CONNECT:
|
||||||
- No gated Discord. No paywalls. No gated content.
|
Website: https://bmadcode.com/
|
||||||
- We believe in empowering everyone, not just those who can pay.
|
Discord: https://discord.gg/gk8jAdXWmj
|
||||||
- Knowledge should be shared, not sold.
|
YouTube: https://www.youtube.com/@BMadCode
|
||||||
|
X: https://x.com/BMadCode
|
||||||
|
Facebook: https://facebook.com/@BMadCode
|
||||||
|
|
||||||
🎤 SPEAKING & MEDIA:
|
⭐ SUPPORT THE PROJECT:
|
||||||
- Available for conferences, podcasts, and media appearances
|
Star us: https://github.com/bmad-code-org/BMAD-METHOD/
|
||||||
- Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
|
Donate: https://buymeacoffee.com/bmad
|
||||||
- For speaking inquiries or interviews, reach out to BMad on Discord!
|
Corporate sponsorship and speaking inquiries: contact@bmadcode.com
|
||||||
|
|
||||||
⭐ HELP US GROW:
|
Docs, blog, and latest updates: https://bmadcode.com/
|
||||||
- Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
|
|
||||||
- Subscribe on YouTube: https://www.youtube.com/@BMadCode
|
|
||||||
- Free Community and Support: https://discord.gg/gk8jAdXWmj
|
|
||||||
- Donate: https://buymeacoffee.com/bmad
|
|
||||||
- Corporate Sponsorship available
|
|
||||||
|
|
||||||
Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/blob/main/CHANGELOG.md
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,50 @@ const fs = require('fs-extra');
|
||||||
const { CLIUtils } = require('./cli-utils');
|
const { CLIUtils } = require('./cli-utils');
|
||||||
const { CustomHandler } = require('./custom-handler');
|
const { CustomHandler } = require('./custom-handler');
|
||||||
const { ExternalModuleManager } = require('./modules/external-manager');
|
const { ExternalModuleManager } = require('./modules/external-manager');
|
||||||
|
const { getProjectRoot } = require('./project-root');
|
||||||
const prompts = require('./prompts');
|
const prompts = require('./prompts');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read module version from .claude-plugin/marketplace.json
|
||||||
|
* @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis')
|
||||||
|
* @returns {string} Version string or empty string
|
||||||
|
*/
|
||||||
|
async function getMarketplaceVersion(moduleCode) {
|
||||||
|
let marketplacePath;
|
||||||
|
if (moduleCode === 'core' || moduleCode === 'bmm') {
|
||||||
|
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
||||||
|
} else {
|
||||||
|
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleCode);
|
||||||
|
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (await fs.pathExists(marketplacePath)) {
|
||||||
|
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||||
|
return _extractMarketplaceVersion(data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the highest version from marketplace.json plugins array.
|
||||||
|
* Handles multiple plugins per file safely.
|
||||||
|
* @param {Object} data - Parsed marketplace.json
|
||||||
|
* @returns {string} Version string or empty string
|
||||||
|
*/
|
||||||
|
function _extractMarketplaceVersion(data) {
|
||||||
|
const plugins = data?.plugins;
|
||||||
|
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
||||||
|
// Use the highest version across all plugins in the file
|
||||||
|
let best = '';
|
||||||
|
for (const p of plugins) {
|
||||||
|
if (p.version && (!best || p.version > best)) best = p.version;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
// Separator class for visual grouping in select/multiselect prompts
|
// Separator class for visual grouping in select/multiselect prompts
|
||||||
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
||||||
class Separator {
|
class Separator {
|
||||||
|
|
@ -70,17 +112,14 @@ class UI {
|
||||||
if (hasExistingInstall) {
|
if (hasExistingInstall) {
|
||||||
// Get version information
|
// Get version information
|
||||||
const { existingInstall, bmadDir } = await this.getExistingInstallation(confirmedDirectory);
|
const { existingInstall, bmadDir } = await this.getExistingInstallation(confirmedDirectory);
|
||||||
const packageJsonPath = path.join(__dirname, '../../package.json');
|
|
||||||
const currentVersion = require(packageJsonPath).version;
|
|
||||||
const installedVersion = existingInstall.installed ? existingInstall.version || 'unknown' : 'unknown';
|
|
||||||
|
|
||||||
// Build menu choices dynamically
|
// Build menu choices dynamically
|
||||||
const choices = [];
|
const choices = [];
|
||||||
|
|
||||||
// Always show Quick Update first (allows refreshing installation even on same version)
|
// Always show Quick Update first (allows refreshing installation even on same version)
|
||||||
if (installedVersion !== 'unknown') {
|
if (existingInstall.installed) {
|
||||||
choices.push({
|
choices.push({
|
||||||
name: `Quick Update (v${installedVersion} → v${currentVersion})`,
|
name: 'Quick Update',
|
||||||
value: 'quick-update',
|
value: 'quick-update',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -880,14 +919,18 @@ class UI {
|
||||||
const lockedValues = ['core'];
|
const lockedValues = ['core'];
|
||||||
|
|
||||||
// Core module is always installed — show it locked at the top
|
// Core module is always installed — show it locked at the top
|
||||||
allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' });
|
const coreVersion = await getMarketplaceVersion('core');
|
||||||
|
const coreLabel = coreVersion ? `BMad Core Module (v${coreVersion})` : 'BMad Core Module';
|
||||||
|
allOptions.push({ label: coreLabel, value: 'core', hint: 'Core configuration and shared resources' });
|
||||||
initialValues.push('core');
|
initialValues.push('core');
|
||||||
|
|
||||||
// Helper to build module entry with proper sorting and selection
|
// Helper to build module entry with proper sorting and selection
|
||||||
const buildModuleEntry = (mod, value, group) => {
|
const buildModuleEntry = async (mod, value, group) => {
|
||||||
const isInstalled = installedModuleIds.has(value);
|
const isInstalled = installedModuleIds.has(value);
|
||||||
|
const version = await getMarketplaceVersion(value);
|
||||||
|
const label = version ? `${mod.name} (v${version})` : mod.name;
|
||||||
return {
|
return {
|
||||||
label: mod.name,
|
label,
|
||||||
value,
|
value,
|
||||||
hint: mod.description || group,
|
hint: mod.description || group,
|
||||||
// Pre-select only if already installed (not on fresh install)
|
// Pre-select only if already installed (not on fresh install)
|
||||||
|
|
@ -899,7 +942,7 @@ class UI {
|
||||||
const localEntries = [];
|
const localEntries = [];
|
||||||
for (const mod of localModules) {
|
for (const mod of localModules) {
|
||||||
if (!mod.isCustom && mod.id !== 'core') {
|
if (!mod.isCustom && mod.id !== 'core') {
|
||||||
const entry = buildModuleEntry(mod, mod.id, 'Local');
|
const entry = await buildModuleEntry(mod, mod.id, 'Local');
|
||||||
localEntries.push(entry);
|
localEntries.push(entry);
|
||||||
if (entry.selected) {
|
if (entry.selected) {
|
||||||
initialValues.push(mod.id);
|
initialValues.push(mod.id);
|
||||||
|
|
@ -912,7 +955,7 @@ class UI {
|
||||||
const officialModules = [];
|
const officialModules = [];
|
||||||
for (const mod of externalModules) {
|
for (const mod of externalModules) {
|
||||||
if (mod.type === 'bmad-org') {
|
if (mod.type === 'bmad-org') {
|
||||||
const entry = buildModuleEntry(mod, mod.code, 'Official');
|
const entry = await buildModuleEntry(mod, mod.code, 'Official');
|
||||||
officialModules.push(entry);
|
officialModules.push(entry);
|
||||||
if (entry.selected) {
|
if (entry.selected) {
|
||||||
initialValues.push(mod.code);
|
initialValues.push(mod.code);
|
||||||
|
|
@ -925,7 +968,7 @@ class UI {
|
||||||
const communityModules = [];
|
const communityModules = [];
|
||||||
for (const mod of externalModules) {
|
for (const mod of externalModules) {
|
||||||
if (mod.type === 'community') {
|
if (mod.type === 'community') {
|
||||||
const entry = buildModuleEntry(mod, mod.code, 'Community');
|
const entry = await buildModuleEntry(mod, mod.code, 'Community');
|
||||||
communityModules.push(entry);
|
communityModules.push(entry);
|
||||||
if (entry.selected) {
|
if (entry.selected) {
|
||||||
initialValues.push(mod.code);
|
initialValues.push(mod.code);
|
||||||
|
|
|
||||||
|
|
@ -156,8 +156,15 @@ function mapInstalledToSource(refPath) {
|
||||||
// Skip install-only paths (generated at install time, not in source)
|
// Skip install-only paths (generated at install time, not in source)
|
||||||
if (isInstallOnly(cleaned)) return null;
|
if (isInstallOnly(cleaned)) return null;
|
||||||
|
|
||||||
// core/, bmm/, and utility/ are directly under src/
|
// Map installed module names to their source directory names
|
||||||
if (cleaned.startsWith('core/') || cleaned.startsWith('bmm/') || cleaned.startsWith('utility/')) {
|
// _bmad/core/ → src/core-skills/, _bmad/bmm/ → src/bmm-skills/
|
||||||
|
if (cleaned.startsWith('core/')) {
|
||||||
|
return path.join(SRC_DIR, 'core-skills', cleaned.slice('core/'.length));
|
||||||
|
}
|
||||||
|
if (cleaned.startsWith('bmm/')) {
|
||||||
|
return path.join(SRC_DIR, 'bmm-skills', cleaned.slice('bmm/'.length));
|
||||||
|
}
|
||||||
|
if (cleaned.startsWith('utility/')) {
|
||||||
return path.join(SRC_DIR, cleaned);
|
return path.join(SRC_DIR, cleaned);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue