Compare commits
4 Commits
c32d69a48f
...
6c0fdf705e
| Author | SHA1 | Date |
|---|---|---|
|
|
6c0fdf705e | |
|
|
c46502f640 | |
|
|
beeccbc29c | |
|
|
0416f72d7d |
|
|
@ -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
|
||||
|
|
@ -34,6 +34,7 @@ Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|||
- `ux_file` = `{planning_artifacts}/*ux*.md`
|
||||
- `story_title` = "" (will be elicited if not derivable)
|
||||
- `project_context` = `**/project-context.md` (load if exists)
|
||||
- `deferred_work_file` = `{implementation_artifacts}/deferred-work.md`
|
||||
- `default_output_file` = `{implementation_artifacts}/{{story_key}}.md`
|
||||
|
||||
### Input Files
|
||||
|
|
@ -44,6 +45,7 @@ Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|||
| architecture | Architecture (fallback - epics file should have relevant sections) | whole: `{planning_artifacts}/*architecture*.md`, sharded: `{planning_artifacts}/*architecture*/*.md` | SELECTIVE_LOAD |
|
||||
| ux | UX design (fallback - epics file should have relevant sections) | whole: `{planning_artifacts}/*ux*.md`, sharded: `{planning_artifacts}/*ux*/*.md` | SELECTIVE_LOAD |
|
||||
| epics | Enhanced epics+stories file with BDD and source hints | whole: `{planning_artifacts}/*epic*.md`, sharded: `{planning_artifacts}/*epic*/*.md` | SELECTIVE_LOAD |
|
||||
| deferred_work | Deferred items from code reviews (optional) | `{deferred_work_file}` | FULL_LOAD (optional) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -232,6 +234,54 @@ Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|||
all learnings that could impact current story implementation</action>
|
||||
</check>
|
||||
|
||||
<!-- Deferred work items analysis -->
|
||||
<check if="{deferred_work_file} exists AND has content">
|
||||
<action>Load {deferred_work_file} completely</action>
|
||||
<action>Parse all deferred items. The file uses level-2 headings produced by bmad-code-review:
|
||||
`## Deferred from: code review of story-X.Y (YYYY-MM-DD)`
|
||||
Each heading is followed by bullet items (one per deferred finding).
|
||||
|
||||
For each bullet item extract:
|
||||
- File paths mentioned (e.g., [src/foo.ts:42])
|
||||
- Originating review: the heading text above the bullet (e.g., "code review of story-2.3 (2026-03-18)")
|
||||
- Description text: the bullet content
|
||||
- Category: if the producer included an explicit category, use it; otherwise derive heuristically from keywords in the description:
|
||||
- "security" / "auth" / "injection" / "XSS" / "CSRF" → security
|
||||
- "bug" / "crash" / "error" / "null" / "undefined" / "NaN" → bug
|
||||
- "performance" / "slow" / "N+1" / "cache" → performance
|
||||
- "style" / "lint" / "formatting" / "naming" → style
|
||||
- otherwise → tech-debt
|
||||
- Set `inferred_category = true` when the category was derived heuristically
|
||||
</action>
|
||||
|
||||
<action>From epics content and architecture analysis, build a list of files this story will likely touch:
|
||||
- Files explicitly mentioned in story requirements
|
||||
- Files in modules/directories related to the story's feature area
|
||||
- Files that share dependencies with story components
|
||||
</action>
|
||||
|
||||
<action>Match deferred items against the story's file list:
|
||||
- EXACT match: deferred item references a file the story will modify
|
||||
- DIRECTORY match: deferred item is in the same directory/module
|
||||
- COMPONENT match: deferred item affects a component the story depends on
|
||||
</action>
|
||||
|
||||
<check if="overlapping deferred items found">
|
||||
<action>Store {{matched_deferred_items}} for inclusion in the story file</action>
|
||||
<action>Set {{matched_count}} = number of items in {{matched_deferred_items}}</action>
|
||||
<action>Classify matches by priority:
|
||||
- HIGH: security fixes, bugs in files this story will modify
|
||||
- MEDIUM: tech-debt in the same module, performance issues in touched code
|
||||
- LOW: style issues, minor refactors in adjacent files
|
||||
</action>
|
||||
<output>📋 Found {{matched_count}} deferred work items relevant to this story from previous code reviews</output>
|
||||
</check>
|
||||
|
||||
<check if="no overlapping deferred items found">
|
||||
<action>Set {{matched_deferred_items}} = empty</action>
|
||||
</check>
|
||||
</check>
|
||||
|
||||
<!-- Git intelligence for previous work patterns -->
|
||||
<check
|
||||
if="previous story exists AND git repository detected">
|
||||
|
|
@ -324,6 +374,24 @@ Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|||
<template-output file="{default_output_file}">git_intelligence_summary</template-output>
|
||||
</check>
|
||||
|
||||
<!-- Deferred work items from previous code reviews -->
|
||||
<check if="{{matched_deferred_items}} is not empty">
|
||||
<action>In the Dev Notes section, add a subsection:</action>
|
||||
<template-output file="{default_output_file}">
|
||||
### Deferred Items to Address
|
||||
|
||||
The following items were deferred from previous code reviews and overlap with files/modules this story will touch. Address these during implementation where practical.
|
||||
|
||||
{{#each matched_deferred_items}}
|
||||
- **[{{priority}}]** {{description}} `[{{file_ref}}]` — _from {{origin_review}}_
|
||||
{{/each}}
|
||||
</template-output>
|
||||
|
||||
<action>In the Tasks/Subtasks section, add corresponding subtasks for HIGH-priority deferred items:
|
||||
- [ ] [Deferred] {{description}} [{{file_ref}}] (from previous review)
|
||||
</action>
|
||||
</check>
|
||||
|
||||
<!-- Latest technical specifics -->
|
||||
<check if="web research completed">
|
||||
<template-output file="{default_output_file}">latest_tech_information</template-output>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|||
### Paths
|
||||
|
||||
- `sprint_status_file` = `{implementation_artifacts}/sprint-status.yaml`
|
||||
- `deferred_work_file` = `{implementation_artifacts}/deferred-work.md`
|
||||
|
||||
### Input Files
|
||||
|
||||
|
|
@ -48,6 +49,7 @@ Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|||
| architecture | System architecture for context | whole: `{planning_artifacts}/*architecture*.md`, sharded: `{planning_artifacts}/*architecture*/*.md` | FULL_LOAD |
|
||||
| prd | Product requirements for context | whole: `{planning_artifacts}/*prd*.md`, sharded: `{planning_artifacts}/*prd*/*.md` | FULL_LOAD |
|
||||
| document_project | Brownfield project documentation (optional) | sharded: `{planning_artifacts}/*.md` | INDEX_GUIDED |
|
||||
| deferred_work | Deferred items from code reviews | `{deferred_work_file}` | FULL_LOAD (optional) |
|
||||
|
||||
### Required Inputs
|
||||
|
||||
|
|
@ -247,6 +249,41 @@ Charlie (Senior Dev): "Good idea - those dev notes always have gold in them."
|
|||
- Track bug patterns or regression issues
|
||||
- Document test coverage gaps
|
||||
|
||||
**Deferred Work Backlog Analysis:**
|
||||
|
||||
<check if="{deferred_work_file} exists AND has content">
|
||||
<action>Load {deferred_work_file} completely</action>
|
||||
<action>Parse all deferred items and compute:</action>
|
||||
|
||||
- Total items deferred across all reviews
|
||||
- Items originating from this epic's stories (match by level-2 headings: `## Deferred from: code review of story-{{epic_number}}.* (YYYY-MM-DD)`)
|
||||
- Items originating from previous epics (carried forward — headings referencing other epic numbers)
|
||||
- Items that were addressed during this epic (cross-reference with story file lists and git history)
|
||||
- Items still outstanding
|
||||
|
||||
<action>Classify outstanding items by severity. If the producer included an explicit category use it; otherwise derive heuristically from description keywords (security/auth/injection → security; bug/crash/error/null → bug; performance/slow/cache → performance; style/lint/naming → style; default → tech-debt):</action>
|
||||
|
||||
- Security issues: count and list
|
||||
- Bugs: count and list
|
||||
- Tech-debt: count and list
|
||||
- Style/minor: count and list
|
||||
|
||||
<action>Store deferred work stats:</action>
|
||||
|
||||
- {{deferred_created_this_epic}}: items deferred during this epic's reviews
|
||||
- {{deferred_resolved_this_epic}}: items addressed during this epic
|
||||
- {{deferred_carried_forward}}: items still outstanding
|
||||
- {{deferred_high_priority}}: security + bug items still outstanding
|
||||
|
||||
<action>IF {{deferred_carried_forward}} > 0: flag for discussion in retrospective as a quality concern</action>
|
||||
<action>IF {{deferred_high_priority}} > 0: flag as critical — high-priority items are aging without resolution</action>
|
||||
</check>
|
||||
|
||||
<check if="{deferred_work_file} does NOT exist or is empty">
|
||||
<action>Set {{deferred_created_this_epic}} = 0, {{deferred_resolved_this_epic}} = 0, {{deferred_carried_forward}} = 0</action>
|
||||
<action>Note: no deferred work file found — either no code reviews ran or all findings were resolved inline</action>
|
||||
</check>
|
||||
|
||||
<action>Synthesize patterns across all stories:</action>
|
||||
|
||||
**Common Struggles:**
|
||||
|
|
@ -507,6 +544,13 @@ Quality and Technical:
|
|||
- Test coverage: {{coverage_info}}
|
||||
- Production incidents: {{incident_count}}
|
||||
|
||||
Deferred Work (from code reviews):
|
||||
|
||||
- Created this epic: {{deferred_created_this_epic}}
|
||||
- Resolved this epic: {{deferred_resolved_this_epic}}
|
||||
- Carried forward: {{deferred_carried_forward}}{{#if deferred_high_priority}}
|
||||
- ⚠️ High-priority outstanding: {{deferred_high_priority}} (security/bugs){{/if}}
|
||||
|
||||
Business Outcomes:
|
||||
|
||||
- Goals achieved: {{goals_met}}/{{total_goals}}
|
||||
|
|
@ -1352,6 +1396,7 @@ Amelia (Developer): "See you all when prep work is done. Meeting adjourned!"
|
|||
- Action items with owners and timelines
|
||||
- Preparation tasks for next epic
|
||||
- Critical path items
|
||||
- Deferred work summary (items created, resolved, carried forward, high-priority outstanding)
|
||||
- Significant discoveries and epic update recommendations (if any)
|
||||
- Readiness assessment
|
||||
- Commitments and next steps
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|||
### Paths
|
||||
|
||||
- `sprint_status_file` = `{implementation_artifacts}/sprint-status.yaml`
|
||||
- `deferred_work_file` = `{implementation_artifacts}/deferred-work.md`
|
||||
|
||||
### Input Files
|
||||
|
||||
| Input | Path | Load Strategy |
|
||||
|-------|------|---------------|
|
||||
| Sprint status | `{sprint_status_file}` | FULL_LOAD |
|
||||
| Deferred work | `{deferred_work_file}` | FULL_LOAD (optional) |
|
||||
|
||||
### Context
|
||||
|
||||
|
|
@ -118,6 +120,25 @@ Enter corrections (e.g., "1=in-progress, 2=backlog") or "skip" to continue witho
|
|||
- IF `last_updated` timestamp is more than 7 days old (or `last_updated` is missing, fall back to `generated`): warn "sprint-status.yaml may be stale"
|
||||
- IF any story key doesn't match an epic pattern (e.g., story "5-1-..." but no "epic-5"): warn "orphaned story detected"
|
||||
- IF any epic has status in-progress but has no associated stories: warn "in-progress epic has no stories"
|
||||
|
||||
<action>Analyze deferred work backlog (if {deferred_work_file} exists):</action>
|
||||
|
||||
<check if="{deferred_work_file} exists AND has content">
|
||||
<action>Parse all deferred items from {deferred_work_file}. The file uses level-2 headings produced by bmad-code-review:
|
||||
`## Deferred from: code review of story-X.Y (YYYY-MM-DD)`
|
||||
Each heading is followed by bullet items (one per deferred finding).
|
||||
</action>
|
||||
<action>Count total deferred items (bullet items, not headings)</action>
|
||||
<action>Group items by originating review/story (derived from the heading above each group)</action>
|
||||
<action>Classify items by severity: if the item includes an explicit category use it; otherwise derive heuristically from description keywords (security/auth/injection → security; bug/crash/error/null → bug; performance/slow/cache → performance; style/lint/naming → style; default → tech-debt)</action>
|
||||
<action>Store counts: {{deferred_total}}, {{deferred_high}} (security/bug), {{deferred_medium}} (tech-debt/performance), {{deferred_low}} (style/minor)</action>
|
||||
<action>IF {{deferred_total}} > 20: add risk "Deferred work backlog is large ({{deferred_total}} items) — consider triaging with SM agent"</action>
|
||||
<action>IF {{deferred_high}} > 0: add risk "{{deferred_high}} high-priority deferred items (security/bugs) need attention"</action>
|
||||
</check>
|
||||
|
||||
<check if="{deferred_work_file} does NOT exist OR is empty">
|
||||
<action>Set {{deferred_total}} = 0, {{deferred_high}} = 0, {{deferred_medium}} = 0, {{deferred_low}} = 0</action>
|
||||
</check>
|
||||
</step>
|
||||
|
||||
<step n="3" goal="Select next action recommendation">
|
||||
|
|
@ -144,6 +165,10 @@ Enter corrections (e.g., "1=in-progress, 2=backlog") or "skip" to continue witho
|
|||
|
||||
**Epics:** backlog {{epic_backlog}}, in-progress {{epic_in_progress}}, done {{epic_done}}
|
||||
|
||||
{{#if deferred_total}}
|
||||
**Deferred Work:** {{deferred_total}} items ({{deferred_high}} high, {{deferred_medium}} medium, {{deferred_low}} low)
|
||||
{{/if}}
|
||||
|
||||
**Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}})
|
||||
|
||||
{{#if risks}}
|
||||
|
|
@ -195,7 +220,7 @@ If the command targets a story, set `story_key={{next_story_id}}` when prompted.
|
|||
<!-- ========================= -->
|
||||
|
||||
<step n="20" goal="Data mode output">
|
||||
<action>Load and parse {sprint_status_file} same as Step 2</action>
|
||||
<action>Load and parse {sprint_status_file} same as Step 2 (including deferred work analysis — set deferred counts to 0 when file is missing/empty)</action>
|
||||
<action>Compute recommendation same as Step 3</action>
|
||||
<template-output>next_workflow_id = {{next_workflow_id}}</template-output>
|
||||
<template-output>next_story_id = {{next_story_id}}</template-output>
|
||||
|
|
@ -208,6 +233,10 @@ If the command targets a story, set `story_key={{next_story_id}}` when prompted.
|
|||
<template-output>epic_in_progress = {{epic_in_progress}}</template-output>
|
||||
<template-output>epic_done = {{epic_done}}</template-output>
|
||||
<template-output>risks = {{risks}}</template-output>
|
||||
<template-output>deferred_total = {{deferred_total}}</template-output>
|
||||
<template-output>deferred_high = {{deferred_high}}</template-output>
|
||||
<template-output>deferred_medium = {{deferred_medium}}</template-output>
|
||||
<template-output>deferred_low = {{deferred_low}}</template-output>
|
||||
<action>Return to caller</action>
|
||||
</step>
|
||||
|
||||
|
|
|
|||
|
|
@ -1301,6 +1301,14 @@ async function runTests() {
|
|||
'---\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","true"\n',
|
||||
);
|
||||
|
||||
// Run Claude Code setup (which triggers cleanup then install)
|
||||
const ideManager27 = new IdeManager();
|
||||
await ideManager27.ensureInitialized();
|
||||
|
|
|
|||
|
|
@ -19,24 +19,33 @@ const CLIUtils = {
|
|||
* Display BMAD logo and version using @clack intro + box
|
||||
*/
|
||||
async displayLogo() {
|
||||
const version = this.getVersion();
|
||||
const color = await prompts.getColor();
|
||||
const termWidth = process.stdout.columns || 80;
|
||||
|
||||
// ASCII art logo
|
||||
const logo = [
|
||||
// Full "BMad Method" logo for wide terminals, "BMad" only for narrow
|
||||
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',
|
||||
rounded: true,
|
||||
formatBorder: color.blue,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,44 @@ class Installer {
|
|||
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
|
||||
* @param {Object} config - Installation configuration
|
||||
|
|
@ -52,9 +90,36 @@ class Installer {
|
|||
|
||||
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
|
||||
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);
|
||||
|
||||
|
|
@ -65,7 +130,7 @@ class Installer {
|
|||
|
||||
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);
|
||||
|
||||
const restoreResult = await this._restoreUserFiles(paths, updateState);
|
||||
|
||||
|
|
@ -76,6 +141,7 @@ class Installer {
|
|||
ides: config.ides,
|
||||
customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
|
||||
modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
|
||||
preInstallVersions,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -321,7 +387,7 @@ class Installer {
|
|||
/**
|
||||
* 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;
|
||||
|
||||
await this.ideManager.ensureInitialized();
|
||||
|
|
@ -336,6 +402,7 @@ class Installer {
|
|||
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
|
||||
selectedModules: allModules || [],
|
||||
verbose: config.verbose,
|
||||
previousSkillIds,
|
||||
});
|
||||
|
||||
if (setupResult.success) {
|
||||
|
|
@ -556,7 +623,7 @@ class Installer {
|
|||
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
||||
|
||||
const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
|
||||
await officialModules.install(
|
||||
const installResult = await officialModules.install(
|
||||
moduleName,
|
||||
paths.bmadDir,
|
||||
(filePath) => {
|
||||
|
|
@ -570,7 +637,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 +670,11 @@ class Installer {
|
|||
[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 +1138,10 @@ class Installer {
|
|||
const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
|
||||
|
||||
// Build step lines with status indicators
|
||||
const preVersions = context.preInstallVersions || new Map();
|
||||
const lines = [];
|
||||
for (const r of results) {
|
||||
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;
|
||||
}
|
||||
const stepLabel = r.step;
|
||||
|
||||
let icon;
|
||||
if (r.status === 'ok') {
|
||||
|
|
@ -1088,18 +1151,32 @@ class Installer {
|
|||
} else {
|
||||
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}`);
|
||||
}
|
||||
|
||||
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
|
||||
lines.push('');
|
||||
if (context.bmadDir) {
|
||||
lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
|
||||
lines.push(` Installed to: ${context.bmadDir}`);
|
||||
}
|
||||
if (context.customFiles && context.customFiles.length > 0) {
|
||||
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
|
||||
|
|
@ -1111,17 +1188,18 @@ class Installer {
|
|||
// Next steps
|
||||
lines.push(
|
||||
'',
|
||||
' Next steps:',
|
||||
` Read our new Docs Site: ${color.dim('https://docs.bmad-method.org/')}`,
|
||||
` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
|
||||
` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`,
|
||||
` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`,
|
||||
' Get started:',
|
||||
` 1. Launch your AI agent from your project folder`,
|
||||
` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
|
||||
'',
|
||||
` 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 +1309,7 @@ class Installer {
|
|||
}
|
||||
|
||||
for (const moduleName of modulesToUpdate) {
|
||||
if (moduleName === 'core') continue; // Already collected above
|
||||
const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
|
||||
if (modulePrompted) {
|
||||
promptedForNewFields = true;
|
||||
|
|
|
|||
|
|
@ -837,14 +837,13 @@ class Manifest {
|
|||
* @returns {Object} Version info object with version, source, npmPackage, repoUrl
|
||||
*/
|
||||
async getModuleVersionInfo(moduleName, bmadDir, moduleSourcePath = null) {
|
||||
const os = require('node:os');
|
||||
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)) {
|
||||
const bmadVersion = require(path.join(getProjectRoot(), 'package.json')).version;
|
||||
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
||||
return {
|
||||
version: bmadVersion,
|
||||
version,
|
||||
source: 'built-in',
|
||||
npmPackage: null,
|
||||
repoUrl: null,
|
||||
|
|
@ -857,42 +856,20 @@ class Manifest {
|
|||
const moduleInfo = await extMgr.getModuleByCode(moduleName);
|
||||
|
||||
if (moduleInfo) {
|
||||
// External module - try to get version from npm registry first, then fall back to cache
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External module: use moduleSourcePath if provided, otherwise fall back to cache
|
||||
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
||||
return {
|
||||
version: version,
|
||||
version,
|
||||
source: 'external',
|
||||
npmPackage: moduleInfo.npmPackage || 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 moduleYamlPath = path.join(cacheDir, 'module.yaml');
|
||||
|
||||
|
|
@ -901,7 +878,7 @@ class Manifest {
|
|||
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
||||
const moduleConfig = yaml.parse(yamlContent);
|
||||
return {
|
||||
version: moduleConfig.version || null,
|
||||
version: version || moduleConfig.version || null,
|
||||
source: 'custom',
|
||||
npmPackage: moduleConfig.npmPackage || null,
|
||||
repoUrl: moduleConfig.repoUrl || null,
|
||||
|
|
@ -913,13 +890,62 @@ class Manifest {
|
|||
|
||||
// Unknown module
|
||||
return {
|
||||
version: null,
|
||||
version,
|
||||
source: 'unknown',
|
||||
npmPackage: 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
|
||||
* @param {string} packageName - npm package name
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class ConfigDrivenIdeSetup {
|
|||
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
|
||||
|
||||
// Clean up any old BMAD installation first
|
||||
await this.cleanup(projectDir, options);
|
||||
await this.cleanup(projectDir, options, bmadDir);
|
||||
|
||||
if (!this.installerConfig) {
|
||||
return { success: false, reason: 'no-config' };
|
||||
|
|
@ -215,15 +215,34 @@ class ConfigDrivenIdeSetup {
|
|||
* Cleanup IDE configuration
|
||||
* @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)
|
||||
// Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
|
||||
if (this.installerConfig?.legacy_targets) {
|
||||
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
||||
for (const legacyDir of this.installerConfig.legacy_targets) {
|
||||
if (this.isGlobalPath(legacyDir)) {
|
||||
await this.warnGlobalLegacy(legacyDir, options);
|
||||
} else {
|
||||
await this.cleanupTarget(projectDir, legacyDir, options);
|
||||
await this.cleanupTarget(projectDir, legacyDir, options, null);
|
||||
await this.removeEmptyParents(projectDir, legacyDir);
|
||||
}
|
||||
}
|
||||
|
|
@ -244,9 +263,9 @@ class ConfigDrivenIdeSetup {
|
|||
await this.cleanupRovoDevPrompts(projectDir, options);
|
||||
}
|
||||
|
||||
// Clean target directory
|
||||
// Clean current target directory
|
||||
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 +305,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} 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);
|
||||
|
||||
if (!(await fs.pathExists(targetPath))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all bmad* files
|
||||
if (removalSet && removalSet.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(targetPath);
|
||||
} catch {
|
||||
// Directory exists but can't be read - skip cleanup
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -313,23 +426,26 @@ class ConfigDrivenIdeSetup {
|
|||
let removedCount = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry || typeof entry !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (entry.startsWith('bmad') && !entry.startsWith('bmad-os-')) {
|
||||
const entryPath = path.join(targetPath, entry);
|
||||
if (!entry || typeof entry !== 'string') continue;
|
||||
|
||||
// Always preserve bmad-os-* utility skills regardless of cleanup mode
|
||||
if (entry.startsWith('bmad-os-')) continue;
|
||||
|
||||
// Surgical removal from set, or legacy prefix matching when set is null
|
||||
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
|
||||
|
||||
if (shouldRemove) {
|
||||
try {
|
||||
await fs.remove(entryPath);
|
||||
await fs.remove(path.join(targetPath, entry));
|
||||
removedCount++;
|
||||
} catch {
|
||||
// Skip entries that can't be removed (broken symlinks, permission errors)
|
||||
// Skip entries that can't be removed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0 && !options.silent) {
|
||||
await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`);
|
||||
}
|
||||
// Only log cleanup when it's not a routine reinstall (legacy dir cleanup or actual removals)
|
||||
// Suppress for current target_dir since it's always cleaned before a fresh write
|
||||
|
||||
// Remove empty directory after cleanup
|
||||
if (removedCount > 0) {
|
||||
|
|
@ -339,7 +455,7 @@ class ConfigDrivenIdeSetup {
|
|||
await fs.remove(targetPath);
|
||||
}
|
||||
} catch {
|
||||
// Directory may already be gone or in use — skip
|
||||
// Directory may already be gone or in use
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,32 +6,25 @@
|
|||
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!
|
||||
- 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
|
||||
🌟 100% free. 100% open source. Always.
|
||||
No paywalls. No gated content. Knowledge shared, not sold.
|
||||
|
||||
🌟 BMad is 100% free and open source.
|
||||
- No gated Discord. No paywalls. No gated content.
|
||||
- We believe in empowering everyone, not just those who can pay.
|
||||
- Knowledge should be shared, not sold.
|
||||
🌐 CONNECT:
|
||||
Website: https://bmadcode.com/
|
||||
Discord: https://discord.gg/gk8jAdXWmj
|
||||
YouTube: https://www.youtube.com/@BMadCode
|
||||
X: https://x.com/BMadCode
|
||||
Facebook: https://facebook.com/@BMadCode
|
||||
|
||||
🎤 SPEAKING & MEDIA:
|
||||
- Available for conferences, podcasts, and media appearances
|
||||
- Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
|
||||
- For speaking inquiries or interviews, reach out to BMad on Discord!
|
||||
⭐ SUPPORT THE PROJECT:
|
||||
Star us: https://github.com/bmad-code-org/BMAD-METHOD/
|
||||
Donate: https://buymeacoffee.com/bmad
|
||||
Corporate sponsorship and speaking inquiries: contact@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
|
||||
Docs, blog, and latest updates: https://bmadcode.com/
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,50 @@ const fs = require('fs-extra');
|
|||
const { CLIUtils } = require('./cli-utils');
|
||||
const { CustomHandler } = require('./custom-handler');
|
||||
const { ExternalModuleManager } = require('./modules/external-manager');
|
||||
const { getProjectRoot } = require('./project-root');
|
||||
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
|
||||
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
||||
class Separator {
|
||||
|
|
@ -70,17 +112,14 @@ class UI {
|
|||
if (hasExistingInstall) {
|
||||
// Get version information
|
||||
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
|
||||
const choices = [];
|
||||
|
||||
// Always show Quick Update first (allows refreshing installation even on same version)
|
||||
if (installedVersion !== 'unknown') {
|
||||
if (existingInstall.installed) {
|
||||
choices.push({
|
||||
name: `Quick Update (v${installedVersion} → v${currentVersion})`,
|
||||
name: 'Quick Update',
|
||||
value: 'quick-update',
|
||||
});
|
||||
}
|
||||
|
|
@ -880,14 +919,18 @@ class UI {
|
|||
const lockedValues = ['core'];
|
||||
|
||||
// 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');
|
||||
|
||||
// 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 version = await getMarketplaceVersion(value);
|
||||
const label = version ? `${mod.name} (v${version})` : mod.name;
|
||||
return {
|
||||
label: mod.name,
|
||||
label,
|
||||
value,
|
||||
hint: mod.description || group,
|
||||
// Pre-select only if already installed (not on fresh install)
|
||||
|
|
@ -899,7 +942,7 @@ class UI {
|
|||
const localEntries = [];
|
||||
for (const mod of localModules) {
|
||||
if (!mod.isCustom && mod.id !== 'core') {
|
||||
const entry = buildModuleEntry(mod, mod.id, 'Local');
|
||||
const entry = await buildModuleEntry(mod, mod.id, 'Local');
|
||||
localEntries.push(entry);
|
||||
if (entry.selected) {
|
||||
initialValues.push(mod.id);
|
||||
|
|
@ -912,7 +955,7 @@ class UI {
|
|||
const officialModules = [];
|
||||
for (const mod of externalModules) {
|
||||
if (mod.type === 'bmad-org') {
|
||||
const entry = buildModuleEntry(mod, mod.code, 'Official');
|
||||
const entry = await buildModuleEntry(mod, mod.code, 'Official');
|
||||
officialModules.push(entry);
|
||||
if (entry.selected) {
|
||||
initialValues.push(mod.code);
|
||||
|
|
@ -925,7 +968,7 @@ class UI {
|
|||
const communityModules = [];
|
||||
for (const mod of externalModules) {
|
||||
if (mod.type === 'community') {
|
||||
const entry = buildModuleEntry(mod, mod.code, 'Community');
|
||||
const entry = await buildModuleEntry(mod, mod.code, 'Community');
|
||||
communityModules.push(entry);
|
||||
if (entry.selected) {
|
||||
initialValues.push(mod.code);
|
||||
|
|
|
|||
Loading…
Reference in New Issue