Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Verkhovsky 100bb2cc03 refactor(quick-dev): eliminate spec-wip.md singleton
Write directly to spec-{slug}.md with status: draft instead of using
a shared spec-wip.md file. Use draft status for resume detection in
step-01. Removes wipFile variable from all step frontmatter and
workflow initialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:25:58 -07:00
21 changed files with 343 additions and 678 deletions

View File

@ -1,17 +0,0 @@
# 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

View File

@ -1,197 +0,0 @@
# 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.

View File

@ -1,6 +1,6 @@
--- ---
# File references (ONLY variables used in this step) # File references (ONLY variables used in this step)
prdPurpose: '../data/prd-purpose.md' prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
--- ---
# Step E-1: Discovery & Understanding # Step E-1: Discovery & Understanding

View File

@ -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: '../data/prd-purpose.md' prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
--- ---
# Step E-1B: Legacy PRD Conversion Assessment # Step E-1B: Legacy PRD Conversion Assessment

View File

@ -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: '../data/prd-purpose.md' prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
--- ---
# Step E-2: Deep Review & Analysis # Step E-2: Deep Review & Analysis

View File

@ -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: '../data/prd-purpose.md' prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
--- ---
# Step E-3: Edit & Update # Step E-3: Edit & Update

View File

@ -1,6 +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}'
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
@ -116,7 +117,8 @@ 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...**"
- Invoke the `bmad-validate-prd` skill to run the complete validation workflow - Read fully and follow: {validationWorkflow} (steps-v/step-v-01-discovery.md)
- 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**"

View File

@ -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, 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. - 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).
- 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: 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`. 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`.
**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.

View File

@ -11,12 +11,11 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
## INSTRUCTIONS ## INSTRUCTIONS
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. 1. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
2. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._ 2. Read `./spec-template.md` fully. Fill it out based on the intent and investigation, and write the result to `{spec_file}`.
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}`. 3. Self-review against READY FOR DEVELOPMENT standard.
4. Self-review against READY FOR DEVELOPMENT standard. 4. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
5. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human. 5. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
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.

View File

@ -1,5 +1,6 @@
--- ---
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

View File

@ -0,0 +1,154 @@
/**
* 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);
});

View File

@ -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', 'canonicalId,name,description,module,path,install_to_bmad',
'"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md"', '"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md","true"',
'', '',
].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', 'canonicalId,name,description,module,path,install_to_bmad',
'"bmad-help","bmad-help","Native help skill","core","_bmad/core/tasks/bmad-help/SKILL.md"', '"bmad-help","bmad-help","Native help skill","core","_bmad/core/tasks/bmad-help/SKILL.md","true"',
'', '',
].join('\n'), ].join('\n'),
); );
@ -1301,14 +1301,6 @@ 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();

View File

@ -19,33 +19,24 @@ 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;
// Full "BMad Method" logo for wide terminals, "BMad" only for narrow // ASCII art logo
const logoWide = [ const logo = [
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ™',
'██╔══██╗████╗ ████║██╔══██╗██╔══██╗ ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗',
'██████╔╝██╔████╔██║███████║██║ ██║ ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║',
'██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║ ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║',
'██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝',
'╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ',
];
const logoNarrow = [
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™', ' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗', ' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
' ██████╔╝██╔████╔██║███████║██║ ██║', ' ██████╔╝██╔████╔██║███████║██║ ██║',
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║', ' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝', ' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝', ' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
]; ]
.map((line) => color.yellow(line))
.join('\n');
const logoLines = termWidth >= 95 ? logoWide : logoNarrow; const tagline = ' Build More, Architect Dreams';
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}`, '', { await prompts.box(`${logo}\n${tagline}`, `v${version}`, {
contentAlign: 'center', contentAlign: 'center',
rounded: true, rounded: true,
formatBorder: color.blue, formatBorder: color.blue,

View File

@ -26,44 +26,6 @@ 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
@ -90,36 +52,9 @@ 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 = '', meta = {}) => results.push({ step, status, detail, ...meta }); const addResult = (step, status, detail = '') => results.push({ step, status, detail });
// 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);
@ -130,11 +65,7 @@ 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, previousSkillIds); await this._setupIdes(config, allModules, paths, addResult);
// 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);
@ -145,7 +76,6 @@ 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 {
@ -391,7 +321,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, previousSkillIds = new Set()) { async _setupIdes(config, allModules, paths, addResult) {
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();
@ -406,7 +336,6 @@ 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) {
@ -417,33 +346,6 @@ 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).
@ -654,7 +556,7 @@ class Installer {
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`); message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
const moduleConfig = officialModules.moduleConfigs[moduleName] || {}; const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
const installResult = await officialModules.install( await officialModules.install(
moduleName, moduleName,
paths.bmadDir, paths.bmadDir,
(filePath) => { (filePath) => {
@ -668,12 +570,7 @@ class Installer {
}, },
); );
// Get display name from source module.yaml; version from marketplace.json addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
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 });
} }
} }
@ -701,11 +598,7 @@ class Installer {
[moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig }, [moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
}); });
// Get display name from source module.yaml; version from marketplace.json addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
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 });
} }
} }
@ -1169,10 +1062,23 @@ 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) {
const stepLabel = r.step; let stepLabel = null;
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') {
@ -1182,32 +1088,18 @@ 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 (installed in _bmad only)`); lines.push(` ${color.green('\u2713')} No IDE selected ${color.dim('(installed in _bmad only)')}`);
} }
// Context and warnings // Context and warnings
lines.push(''); lines.push('');
if (context.bmadDir) { if (context.bmadDir) {
lines.push(` Installed to: ${context.bmadDir}`); lines.push(` Installed to: ${color.dim(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}`)}`);
@ -1219,18 +1111,17 @@ class Installer {
// Next steps // Next steps
lines.push( lines.push(
'', '',
' Get started:', ' Next steps:',
` 1. Launch your AI agent from your project folder`, ` Read our new Docs Site: ${color.dim('https://docs.bmad-method.org/')}`,
` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`, ` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
'', ` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`,
` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`, ` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`,
` 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.box(lines.join('\n'), 'BMAD is ready to use!', { await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
rounded: true,
formatBorder: color.green,
});
} }
/** /**
@ -1340,7 +1231,6 @@ 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;

View File

@ -9,6 +9,7 @@ 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
@ -41,6 +42,11 @@ 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.
@ -121,7 +127,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 agent metadata. * Manifest YAML is loaded only when present for install_to_bmad and 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() {
@ -150,7 +156,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 agent metadata) // Load manifest when present (for install_to_bmad and 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);
@ -176,6 +182,7 @@ 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
@ -465,7 +472,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\n'; let csvContent = 'canonicalId,name,description,module,path,install_to_bmad\n';
for (const skill of this.skills) { for (const skill of this.skills) {
const row = [ const row = [
@ -474,6 +481,7 @@ 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';
} }

View File

@ -837,13 +837,14 @@ 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');
// Resolve source type first, then read version with the correct path context // Built-in modules use BMad version (only core and bmm are in BMAD-METHOD repo)
if (['core', 'bmm'].includes(moduleName)) { if (['core', 'bmm'].includes(moduleName)) {
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath); const bmadVersion = require(path.join(getProjectRoot(), 'package.json')).version;
return { return {
version, version: bmadVersion,
source: 'built-in', source: 'built-in',
npmPackage: null, npmPackage: null,
repoUrl: null, repoUrl: null,
@ -856,20 +857,42 @@ class Manifest {
const moduleInfo = await extMgr.getModuleByCode(moduleName); const moduleInfo = await extMgr.getModuleByCode(moduleName);
if (moduleInfo) { if (moduleInfo) {
// External module: use moduleSourcePath if provided, otherwise fall back to cache // External module - try to get version from npm registry first, then fall back to cache
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath); let version = null;
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: resolve path from source or cache before reading version // Custom module - check cache directory
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');
@ -878,7 +901,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: version || moduleConfig.version || null, version: moduleConfig.version || null,
source: 'custom', source: 'custom',
npmPackage: moduleConfig.npmPackage || null, npmPackage: moduleConfig.npmPackage || null,
repoUrl: moduleConfig.repoUrl || null, repoUrl: moduleConfig.repoUrl || null,
@ -890,62 +913,13 @@ class Manifest {
// Unknown module // Unknown module
return { return {
version, version: null,
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

View File

@ -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, bmadDir); await this.cleanup(projectDir, options);
if (!this.installerConfig) { if (!this.installerConfig) {
return { success: false, reason: 'no-config' }; return { success: false, reason: 'no-config' };
@ -183,6 +183,18 @@ 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;
} }
@ -203,34 +215,15 @@ class ConfigDrivenIdeSetup {
* Cleanup IDE configuration * Cleanup IDE configuration
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
*/ */
async cleanup(projectDir, options = {}, bmadDir = null) { async cleanup(projectDir, options = {}) {
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, null); await this.cleanupTarget(projectDir, legacyDir, options);
await this.removeEmptyParents(projectDir, legacyDir); await this.removeEmptyParents(projectDir, legacyDir);
} }
} }
@ -251,9 +244,9 @@ class ConfigDrivenIdeSetup {
await this.cleanupRovoDevPrompts(projectDir, options); await this.cleanupRovoDevPrompts(projectDir, options);
} }
// Clean current target directory // Clean target directory
if (this.installerConfig?.target_dir) { if (this.installerConfig?.target_dir) {
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet); await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options);
} }
} }
@ -293,117 +286,23 @@ class ConfigDrivenIdeSetup {
} }
/** /**
* Find the _bmad directory in a project * Cleanup a specific target directory
* @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 = {}, removalSet = new Set()) { async cleanupTarget(projectDir, targetDir, options = {}) {
const targetPath = path.join(projectDir, targetDir); const targetPath = path.join(projectDir, targetDir);
if (!(await fs.pathExists(targetPath))) { if (!(await fs.pathExists(targetPath))) {
return; return;
} }
if (removalSet && removalSet.size === 0) { // Remove all bmad* files
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;
} }
@ -414,26 +313,23 @@ class ConfigDrivenIdeSetup {
let removedCount = 0; let removedCount = 0;
for (const entry of entries) { for (const entry of entries) {
if (!entry || typeof entry !== 'string') continue; if (!entry || typeof entry !== 'string') {
continue;
// Always preserve bmad-os-* utility skills regardless of cleanup mode }
if (entry.startsWith('bmad-os-')) continue; if (entry.startsWith('bmad') && !entry.startsWith('bmad-os-')) {
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(path.join(targetPath, entry)); await fs.remove(entryPath);
removedCount++; removedCount++;
} catch { } catch {
// Skip entries that can't be removed // Skip entries that can't be removed (broken symlinks, permission errors)
} }
} }
} }
// Only log cleanup when it's not a routine reinstall (legacy dir cleanup or actual removals) if (removedCount > 0 && !options.silent) {
// Suppress for current target_dir since it's always cleaned before a fresh write await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`);
}
// Remove empty directory after cleanup // Remove empty directory after cleanup
if (removedCount > 0) { if (removedCount > 0) {
@ -443,7 +339,7 @@ class ConfigDrivenIdeSetup {
await fs.remove(targetPath); await fs.remove(targetPath);
} }
} catch { } catch {
// Directory may already be gone or in use // Directory may already be gone or in use — skip
} }
} }
} }

View File

@ -54,4 +54,19 @@ 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 };

View File

@ -6,25 +6,32 @@
startMessage: | startMessage: |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Agile AI-Driven Development. Powered by BMad Core and a growing module ecosystem. 🎉 V6 IS HERE! Welcome to BMad Method V6 - Official Stable Release!
Install official and community modules during setup to customize your experience.
🌟 100% free. 100% open source. Always. The BMad Method is now a Platform powered by the BMad Method Core and Module Ecosystem!
No paywalls. No gated content. Knowledge shared, not sold. - Select and install modules during setup - customize your experience
- 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
🌐 CONNECT: 🌟 BMad is 100% free and open source.
Website: https://bmadcode.com/ - No gated Discord. No paywalls. No gated content.
Discord: https://discord.gg/gk8jAdXWmj - We believe in empowering everyone, not just those who can pay.
YouTube: https://www.youtube.com/@BMadCode - Knowledge should be shared, not sold.
X: https://x.com/BMadCode
Facebook: https://facebook.com/@BMadCode
⭐ SUPPORT THE PROJECT: 🎤 SPEAKING & MEDIA:
Star us: https://github.com/bmad-code-org/BMAD-METHOD/ - Available for conferences, podcasts, and media appearances
Donate: https://buymeacoffee.com/bmad - Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
Corporate sponsorship and speaking inquiries: contact@bmadcode.com - For speaking inquiries or interviews, reach out to BMad on Discord!
Docs, blog, and latest updates: https://bmadcode.com/ ⭐ HELP US GROW:
- 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
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

View File

@ -4,50 +4,8 @@ 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 {
@ -112,14 +70,17 @@ 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 (existingInstall.installed) { if (installedVersion !== 'unknown') {
choices.push({ choices.push({
name: 'Quick Update', name: `Quick Update (v${installedVersion} → v${currentVersion})`,
value: 'quick-update', value: 'quick-update',
}); });
} }
@ -919,18 +880,14 @@ 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
const coreVersion = await getMarketplaceVersion('core'); allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' });
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 = async (mod, value, group) => { const buildModuleEntry = (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, label: mod.name,
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)
@ -942,7 +899,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 = await buildModuleEntry(mod, mod.id, 'Local'); const entry = 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);
@ -955,7 +912,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 = await buildModuleEntry(mod, mod.code, 'Official'); const entry = 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);
@ -968,7 +925,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 = await buildModuleEntry(mod, mod.code, 'Community'); const entry = 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);

View File

@ -156,15 +156,8 @@ 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;
// Map installed module names to their source directory names // core/, bmm/, and utility/ are directly under src/
// _bmad/core/ → src/core-skills/, _bmad/bmm/ → src/bmm-skills/ if (cleaned.startsWith('core/') || cleaned.startsWith('bmm/') || cleaned.startsWith('utility/')) {
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);
} }