Compare commits
4 Commits
402e25b83d
...
4f55cbd295
| Author | SHA1 | Date |
|---|---|---|
|
|
4f55cbd295 | |
|
|
c46502f640 | |
|
|
33bd6b5d86 | |
|
|
9ff99eceaf |
|
|
@ -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
|
||||||
|
|
@ -53,7 +53,7 @@ context: [] # optional: `{project-root}/`-prefixed paths to project-wide standar
|
||||||
## Tasks & Acceptance
|
## Tasks & Acceptance
|
||||||
|
|
||||||
<!-- Tasks: backtick-quoted file path -- action -- rationale. Prefer one task per file; group tightly-coupled changes when splitting would be artificial. -->
|
<!-- Tasks: backtick-quoted file path -- action -- rationale. Prefer one task per file; group tightly-coupled changes when splitting would be artificial. -->
|
||||||
<!-- If an I/O Matrix is present, include a task to unit-test its edge cases. -->
|
<!-- REQUIRED: Include test tasks for every new or modified behavior. If an I/O Matrix is present, include a task to unit-test its edge cases. -->
|
||||||
<!-- AC covers system-level behaviors not captured by the I/O Matrix. Do not duplicate I/O scenarios here. -->
|
<!-- AC covers system-level behaviors not captured by the I/O Matrix. Do not duplicate I/O scenarios here. -->
|
||||||
|
|
||||||
**Execution:**
|
**Execution:**
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,27 @@ Hand `{spec_file}` to a sub-agent/task and let it implement. If no sub-agents ar
|
||||||
|
|
||||||
**Path formatting rule:** Any markdown links written into `{spec_file}` must use paths relative to `{spec_file}`'s directory so they are clickable in VS Code. Any file paths displayed in terminal/conversation output must use CWD-relative format with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability. No leading `/` in either case.
|
**Path formatting rule:** Any markdown links written into `{spec_file}` must use paths relative to `{spec_file}`'s directory so they are clickable in VS Code. Any file paths displayed in terminal/conversation output must use CWD-relative format with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability. No leading `/` in either case.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
**This is mandatory, not optional.** After implementation, write tests for every new or modified behavior. Follow the project's testing conventions discovered from `{project_context}`, CLAUDE.md, the existing test suite, and any configured test tooling.
|
||||||
|
|
||||||
|
1. **Discover conventions.** Identify the project's test framework, file-naming patterns, and test directory structure in this order:
|
||||||
|
- First, inspect existing tests.
|
||||||
|
- If existing tests are missing or insufficient, inspect the repo's configured test tooling: test scripts, dependencies/devDependencies, and test config files (for example `package.json`, `pytest.ini`, `pyproject.toml`, `jest.config.*`, `vitest.config.*`, `mocha` config, `rspec` config, `go test` conventions, etc.).
|
||||||
|
- Only if neither existing tests nor configured tooling establish conventions should you fall back to the project's language-idiomatic defaults.
|
||||||
|
2. **Write tests.** Cover:
|
||||||
|
- Happy-path behavior for each new or changed feature.
|
||||||
|
- Edge cases and error scenarios from the I/O & Edge-Case Matrix (if present in the spec).
|
||||||
|
- Regressions — any behavior that could break due to the change.
|
||||||
|
3. **Run tests.** Execute the test suite. All new and existing tests must pass. If any test fails, fix the implementation or the test before proceeding.
|
||||||
|
|
||||||
|
If the project has no test infrastructure at all (no existing tests, no test framework, no relevant test config, no test directory, and no test scripts), note this in the spec under `## Verification` and skip — but this is the only acceptable reason to skip tests. Record this skip reason explicitly in the final summary output.
|
||||||
|
|
||||||
### Self-Check
|
### Self-Check
|
||||||
|
|
||||||
Before leaving this step, verify every task in the `## Tasks & Acceptance` section of `{spec_file}` is complete. Mark each finished task `[x]`. If any task is not done, finish it before proceeding.
|
Before leaving this step, verify:
|
||||||
|
1. Every task in the `## Tasks & Acceptance` section of `{spec_file}` is complete. Mark each finished task `[x]`. If any task is not done, finish it before proceeding.
|
||||||
|
2. Tests exist for every new or modified behavior (unless the project has no test infrastructure). If tests are missing, go back and write them before proceeding.
|
||||||
|
|
||||||
## NEXT
|
## NEXT
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ Launch three subagents without conversation context. If no sub-agents are availa
|
||||||
|
|
||||||
- **Blind hunter** — receives `{diff_output}` only. No spec, no context docs, no project access. Invoke via the `bmad-review-adversarial-general` skill.
|
- **Blind hunter** — receives `{diff_output}` only. No spec, no context docs, no project access. Invoke via the `bmad-review-adversarial-general` skill.
|
||||||
- **Edge case hunter** — receives `{diff_output}` and read access to the project. Invoke via the `bmad-review-edge-case-hunter` skill.
|
- **Edge case hunter** — receives `{diff_output}` and read access to the project. Invoke via the `bmad-review-edge-case-hunter` skill.
|
||||||
- **Acceptance auditor** — receives `{diff_output}`, `{spec_file}`, and read access to the project. Must also read the docs listed in `{spec_file}` frontmatter `context`. Checks for violations of acceptance criteria, rules, and principles from the spec and context docs.
|
- **Acceptance auditor** — receives `{diff_output}`, `{spec_file}`, and read access to the project. Must also read the docs listed in `{spec_file}` frontmatter `context`. Checks for violations of acceptance criteria, rules, and principles from the spec and context docs. **Must also verify that tests exist for every new or modified behavior** — missing test coverage for changed code is a `bad_spec` finding unless the project has no test infrastructure.
|
||||||
|
|
||||||
### Classify
|
### Classify
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,19 @@ spec_file: '' # set by step-01 before entering this step
|
||||||
|
|
||||||
Implement the clarified intent directly.
|
Implement the clarified intent directly.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
**This is mandatory, not optional.** After implementation, write tests for every new or modified behavior. Follow the project's testing conventions discovered from `{project_context}`, CLAUDE.md, the existing test suite, and any configured test tooling.
|
||||||
|
|
||||||
|
1. **Discover conventions.** Identify the project's test framework, file-naming patterns, and test directory structure in this order:
|
||||||
|
- First, inspect existing tests.
|
||||||
|
- If existing tests are missing or insufficient, inspect the repo's configured test tooling: test scripts, dependencies/devDependencies, and test config files (for example `package.json`, `pytest.ini`, `pyproject.toml`, `jest.config.*`, `vitest.config.*`, `mocha` config, `rspec` config, `go test` conventions, etc.).
|
||||||
|
- Only if neither existing tests nor configured tooling establish conventions should you fall back to the project's language-idiomatic defaults.
|
||||||
|
2. **Write tests.** Cover happy-path behavior, edge cases, and regressions.
|
||||||
|
3. **Run tests.** All new and existing tests must pass. Fix failures before proceeding.
|
||||||
|
|
||||||
|
If the project has no test infrastructure at all (no existing tests, no test framework, no relevant test config, no test directory, and no test scripts), skip — but this is the only acceptable reason to skip tests. Record this skip reason explicitly in the final summary output.
|
||||||
|
|
||||||
### Review
|
### Review
|
||||||
|
|
||||||
Invoke the `bmad-review-adversarial-general` skill in a subagent with the changed files. The subagent gets NO conversation context — to avoid anchoring bias. If no sub-agents are available, write the changed files to a review prompt file in `{implementation_artifacts}` and HALT. Ask the human to run the review in a separate session and paste back the findings.
|
Invoke the `bmad-review-adversarial-general` skill in a subagent with the changed files. The subagent gets NO conversation context — to avoid anchoring bias. If no sub-agents are available, write the changed files to a review prompt file in `{implementation_artifacts}` and HALT. Ask the human to run the review in a separate session and paste back the findings.
|
||||||
|
|
|
||||||
|
|
@ -1301,6 +1301,14 @@ async function runTests() {
|
||||||
'---\nname: bmad-architect\ndescription: Architect\n---\nOld skill content\n',
|
'---\nname: bmad-architect\ndescription: Architect\n---\nOld skill content\n',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add bmad-architect to the existing skill-manifest.csv so cleanup knows it was previously installed
|
||||||
|
const configDir27 = path.join(installedBmadDir27, '_config');
|
||||||
|
const existingCsv27 = await fs.readFile(path.join(configDir27, 'skill-manifest.csv'), 'utf8');
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(configDir27, 'skill-manifest.csv'),
|
||||||
|
existingCsv27.trimEnd() + '\n"bmad-architect","bmad-architect","Architect","bmm","_bmad/bmm/agents/bmad-architect/SKILL.md","true"\n',
|
||||||
|
);
|
||||||
|
|
||||||
// Run Claude Code setup (which triggers cleanup then install)
|
// Run Claude Code setup (which triggers cleanup then install)
|
||||||
const ideManager27 = new IdeManager();
|
const ideManager27 = new IdeManager();
|
||||||
await ideManager27.ensureInitialized();
|
await ideManager27.ensureInitialized();
|
||||||
|
|
|
||||||
|
|
@ -19,24 +19,33 @@ const CLIUtils = {
|
||||||
* Display BMAD logo and version using @clack intro + box
|
* Display BMAD logo and version using @clack intro + box
|
||||||
*/
|
*/
|
||||||
async displayLogo() {
|
async displayLogo() {
|
||||||
const version = this.getVersion();
|
|
||||||
const color = await prompts.getColor();
|
const color = await prompts.getColor();
|
||||||
|
const termWidth = process.stdout.columns || 80;
|
||||||
|
|
||||||
// ASCII art logo
|
// Full "BMad Method" logo for wide terminals, "BMad" only for narrow
|
||||||
const logo = [
|
const logoWide = [
|
||||||
|
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ™',
|
||||||
|
'██╔══██╗████╗ ████║██╔══██╗██╔══██╗ ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗',
|
||||||
|
'██████╔╝██╔████╔██║███████║██║ ██║ ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║',
|
||||||
|
'██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║ ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║',
|
||||||
|
'██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝',
|
||||||
|
'╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ',
|
||||||
|
];
|
||||||
|
|
||||||
|
const logoNarrow = [
|
||||||
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
|
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
|
||||||
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
|
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
|
||||||
' ██████╔╝██╔████╔██║███████║██║ ██║',
|
' ██████╔╝██╔████╔██║███████║██║ ██║',
|
||||||
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
|
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
|
||||||
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
|
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
|
||||||
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
|
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
|
||||||
]
|
];
|
||||||
.map((line) => color.yellow(line))
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const tagline = ' Build More, Architect Dreams';
|
const logoLines = termWidth >= 95 ? logoWide : logoNarrow;
|
||||||
|
const logo = logoLines.map((line) => color.blue(line)).join('\n');
|
||||||
|
const tagline = color.white(' Build More, Architect Dreams\n © BMad Code');
|
||||||
|
|
||||||
await prompts.box(`${logo}\n${tagline}`, `v${version}`, {
|
await prompts.box(`${logo}\n${tagline}`, '', {
|
||||||
contentAlign: 'center',
|
contentAlign: 'center',
|
||||||
rounded: true,
|
rounded: true,
|
||||||
formatBorder: color.blue,
|
formatBorder: color.blue,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,44 @@ class Installer {
|
||||||
this.bmadFolderName = BMAD_FOLDER_NAME;
|
this.bmadFolderName = BMAD_FOLDER_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the module version from .claude-plugin/marketplace.json
|
||||||
|
* Walks up from sourcePath looking for .claude-plugin/marketplace.json
|
||||||
|
* @param {string} sourcePath - Module source directory
|
||||||
|
* @returns {string} Version string or empty string
|
||||||
|
*/
|
||||||
|
async _getMarketplaceVersion(sourcePath) {
|
||||||
|
let dir = sourcePath;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const marketplacePath = path.join(dir, '.claude-plugin', 'marketplace.json');
|
||||||
|
if (await fs.pathExists(marketplacePath)) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||||
|
return this._extractMarketplaceVersion(data);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parent = path.dirname(dir);
|
||||||
|
if (parent === dir) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the highest version from marketplace.json plugins array
|
||||||
|
*/
|
||||||
|
_extractMarketplaceVersion(data) {
|
||||||
|
const plugins = data?.plugins;
|
||||||
|
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
||||||
|
let best = '';
|
||||||
|
for (const p of plugins) {
|
||||||
|
if (p.version && (!best || p.version > best)) best = p.version;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main installation method
|
* Main installation method
|
||||||
* @param {Object} config - Installation configuration
|
* @param {Object} config - Installation configuration
|
||||||
|
|
@ -52,9 +90,36 @@ class Installer {
|
||||||
|
|
||||||
await this._validateIdeSelection(config);
|
await this._validateIdeSelection(config);
|
||||||
|
|
||||||
|
// Capture pre-install module versions for from→to display
|
||||||
|
const preInstallVersions = new Map();
|
||||||
|
if (existingInstall.installed) {
|
||||||
|
const existingModules = await this.manifest.getAllModuleVersions(paths.bmadDir);
|
||||||
|
for (const mod of existingModules) {
|
||||||
|
if (mod.name && mod.version) {
|
||||||
|
preInstallVersions.set(mod.name, mod.version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Results collector for consolidated summary
|
// Results collector for consolidated summary
|
||||||
const results = [];
|
const results = [];
|
||||||
const addResult = (step, status, detail = '') => results.push({ step, status, detail });
|
const addResult = (step, status, detail = '', meta = {}) => results.push({ step, status, detail, ...meta });
|
||||||
|
|
||||||
|
// Capture previously installed skill IDs before they get overwritten
|
||||||
|
const previousSkillIds = new Set();
|
||||||
|
const prevCsvPath = path.join(paths.bmadDir, '_config', 'skill-manifest.csv');
|
||||||
|
if (await fs.pathExists(prevCsvPath)) {
|
||||||
|
try {
|
||||||
|
const csvParse = require('csv-parse/sync');
|
||||||
|
const content = await fs.readFile(prevCsvPath, 'utf8');
|
||||||
|
const records = csvParse.parse(content, { columns: true, skip_empty_lines: true });
|
||||||
|
for (const r of records) {
|
||||||
|
if (r.canonicalId) previousSkillIds.add(r.canonicalId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this._cacheCustomModules(paths, addResult);
|
await this._cacheCustomModules(paths, addResult);
|
||||||
|
|
||||||
|
|
@ -65,7 +130,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);
|
await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
|
||||||
|
|
||||||
const restoreResult = await this._restoreUserFiles(paths, updateState);
|
const restoreResult = await this._restoreUserFiles(paths, updateState);
|
||||||
|
|
||||||
|
|
@ -76,6 +141,7 @@ class Installer {
|
||||||
ides: config.ides,
|
ides: config.ides,
|
||||||
customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
|
customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
|
||||||
modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
|
modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
|
||||||
|
preInstallVersions,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -321,7 +387,7 @@ class Installer {
|
||||||
/**
|
/**
|
||||||
* Set up IDE integrations for each selected IDE.
|
* Set up IDE integrations for each selected IDE.
|
||||||
*/
|
*/
|
||||||
async _setupIdes(config, allModules, paths, addResult) {
|
async _setupIdes(config, allModules, paths, addResult, previousSkillIds = new Set()) {
|
||||||
if (config.skipIde || !config.ides || config.ides.length === 0) return;
|
if (config.skipIde || !config.ides || config.ides.length === 0) return;
|
||||||
|
|
||||||
await this.ideManager.ensureInitialized();
|
await this.ideManager.ensureInitialized();
|
||||||
|
|
@ -336,6 +402,7 @@ class Installer {
|
||||||
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
|
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
|
||||||
selectedModules: allModules || [],
|
selectedModules: allModules || [],
|
||||||
verbose: config.verbose,
|
verbose: config.verbose,
|
||||||
|
previousSkillIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (setupResult.success) {
|
if (setupResult.success) {
|
||||||
|
|
@ -556,7 +623,7 @@ class Installer {
|
||||||
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
||||||
|
|
||||||
const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
|
const moduleConfig = officialModules.moduleConfigs[moduleName] || {};
|
||||||
await officialModules.install(
|
const installResult = await officialModules.install(
|
||||||
moduleName,
|
moduleName,
|
||||||
paths.bmadDir,
|
paths.bmadDir,
|
||||||
(filePath) => {
|
(filePath) => {
|
||||||
|
|
@ -570,7 +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 },
|
[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()));
|
const selectedIdes = new Set((context.ides || []).map((ide) => String(ide).toLowerCase()));
|
||||||
|
|
||||||
// Build step lines with status indicators
|
// Build step lines with status indicators
|
||||||
|
const preVersions = context.preInstallVersions || new Map();
|
||||||
const lines = [];
|
const lines = [];
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
let stepLabel = null;
|
const stepLabel = r.step;
|
||||||
|
|
||||||
if (r.status !== 'ok') {
|
|
||||||
stepLabel = r.step;
|
|
||||||
} else if (r.step === 'Core') {
|
|
||||||
stepLabel = 'BMAD';
|
|
||||||
} else if (r.step.startsWith('Module: ')) {
|
|
||||||
stepLabel = r.step;
|
|
||||||
} else if (selectedIdes.has(String(r.step).toLowerCase())) {
|
|
||||||
stepLabel = r.step;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stepLabel) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon;
|
let icon;
|
||||||
if (r.status === 'ok') {
|
if (r.status === 'ok') {
|
||||||
|
|
@ -1088,18 +1151,32 @@ class Installer {
|
||||||
} else {
|
} else {
|
||||||
icon = color.red('\u2717');
|
icon = color.red('\u2717');
|
||||||
}
|
}
|
||||||
const detail = r.detail ? color.dim(` (${r.detail})`) : '';
|
|
||||||
|
// Build version detail for module results
|
||||||
|
let detail = '';
|
||||||
|
if (r.moduleCode && r.newVersion) {
|
||||||
|
const oldVersion = preVersions.get(r.moduleCode);
|
||||||
|
if (oldVersion && oldVersion === r.newVersion) {
|
||||||
|
detail = ` (v${r.newVersion}, no change)`;
|
||||||
|
} else if (oldVersion) {
|
||||||
|
detail = ` (v${oldVersion} → v${r.newVersion})`;
|
||||||
|
} else {
|
||||||
|
detail = ` (v${r.newVersion}, installed)`;
|
||||||
|
}
|
||||||
|
} else if (r.detail) {
|
||||||
|
detail = ` (${r.detail})`;
|
||||||
|
}
|
||||||
lines.push(` ${icon} ${stepLabel}${detail}`);
|
lines.push(` ${icon} ${stepLabel}${detail}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((context.ides || []).length === 0) {
|
if ((context.ides || []).length === 0) {
|
||||||
lines.push(` ${color.green('\u2713')} No IDE selected ${color.dim('(installed in _bmad only)')}`);
|
lines.push(` ${color.green('\u2713')} No IDE selected (installed in _bmad only)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context and warnings
|
// Context and warnings
|
||||||
lines.push('');
|
lines.push('');
|
||||||
if (context.bmadDir) {
|
if (context.bmadDir) {
|
||||||
lines.push(` Installed to: ${color.dim(context.bmadDir)}`);
|
lines.push(` Installed to: ${context.bmadDir}`);
|
||||||
}
|
}
|
||||||
if (context.customFiles && context.customFiles.length > 0) {
|
if (context.customFiles && context.customFiles.length > 0) {
|
||||||
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
|
lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`);
|
||||||
|
|
@ -1111,17 +1188,18 @@ class Installer {
|
||||||
// Next steps
|
// Next steps
|
||||||
lines.push(
|
lines.push(
|
||||||
'',
|
'',
|
||||||
' Next steps:',
|
' Get started:',
|
||||||
` Read our new Docs Site: ${color.dim('https://docs.bmad-method.org/')}`,
|
` 1. Launch your AI agent from your project folder`,
|
||||||
` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
|
` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
|
||||||
` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`,
|
'',
|
||||||
` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`,
|
` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
|
||||||
|
` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
|
||||||
);
|
);
|
||||||
if (context.ides && context.ides.length > 0) {
|
|
||||||
lines.push(` Invoke the ${color.cyan('bmad-help')} skill in your IDE Agent to get started`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
|
await prompts.box(lines.join('\n'), 'BMAD is ready to use!', {
|
||||||
|
rounded: true,
|
||||||
|
formatBorder: color.green,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1231,6 +1309,7 @@ class Installer {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const moduleName of modulesToUpdate) {
|
for (const moduleName of modulesToUpdate) {
|
||||||
|
if (moduleName === 'core') continue; // Already collected above
|
||||||
const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
|
const modulePrompted = await quickModules.collectModuleConfigQuick(moduleName, projectDir, true);
|
||||||
if (modulePrompted) {
|
if (modulePrompted) {
|
||||||
promptedForNewFields = true;
|
promptedForNewFields = true;
|
||||||
|
|
|
||||||
|
|
@ -837,14 +837,13 @@ class Manifest {
|
||||||
* @returns {Object} Version info object with version, source, npmPackage, repoUrl
|
* @returns {Object} Version info object with version, source, npmPackage, repoUrl
|
||||||
*/
|
*/
|
||||||
async getModuleVersionInfo(moduleName, bmadDir, moduleSourcePath = null) {
|
async getModuleVersionInfo(moduleName, bmadDir, moduleSourcePath = null) {
|
||||||
const os = require('node:os');
|
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
|
|
||||||
// Built-in modules use BMad version (only core and bmm are in BMAD-METHOD repo)
|
// Resolve source type first, then read version with the correct path context
|
||||||
if (['core', 'bmm'].includes(moduleName)) {
|
if (['core', 'bmm'].includes(moduleName)) {
|
||||||
const bmadVersion = require(path.join(getProjectRoot(), 'package.json')).version;
|
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
||||||
return {
|
return {
|
||||||
version: bmadVersion,
|
version,
|
||||||
source: 'built-in',
|
source: 'built-in',
|
||||||
npmPackage: null,
|
npmPackage: null,
|
||||||
repoUrl: null,
|
repoUrl: null,
|
||||||
|
|
@ -857,42 +856,20 @@ class Manifest {
|
||||||
const moduleInfo = await extMgr.getModuleByCode(moduleName);
|
const moduleInfo = await extMgr.getModuleByCode(moduleName);
|
||||||
|
|
||||||
if (moduleInfo) {
|
if (moduleInfo) {
|
||||||
// External module - try to get version from npm registry first, then fall back to cache
|
// External module: use moduleSourcePath if provided, otherwise fall back to cache
|
||||||
let version = null;
|
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
||||||
|
|
||||||
if (moduleInfo.npmPackage) {
|
|
||||||
// Fetch version from npm registry
|
|
||||||
try {
|
|
||||||
version = await this.fetchNpmVersion(moduleInfo.npmPackage);
|
|
||||||
} catch {
|
|
||||||
// npm fetch failed, try cache as fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If npm didn't work, try reading from cached repo's package.json
|
|
||||||
if (!version) {
|
|
||||||
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
|
|
||||||
const packageJsonPath = path.join(cacheDir, 'package.json');
|
|
||||||
|
|
||||||
if (await fs.pathExists(packageJsonPath)) {
|
|
||||||
try {
|
|
||||||
const pkg = require(packageJsonPath);
|
|
||||||
version = pkg.version;
|
|
||||||
} catch (error) {
|
|
||||||
await prompts.log.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: version,
|
version,
|
||||||
source: 'external',
|
source: 'external',
|
||||||
npmPackage: moduleInfo.npmPackage || null,
|
npmPackage: moduleInfo.npmPackage || null,
|
||||||
repoUrl: moduleInfo.url || null,
|
repoUrl: moduleInfo.url || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom module - check cache directory
|
// Custom module: resolve path from source or cache before reading version
|
||||||
|
const customSourcePath = moduleSourcePath || path.join(bmadDir, '_config', 'custom', moduleName);
|
||||||
|
const version = await this._readMarketplaceVersion(moduleName, customSourcePath);
|
||||||
|
|
||||||
const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName);
|
const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName);
|
||||||
const moduleYamlPath = path.join(cacheDir, 'module.yaml');
|
const moduleYamlPath = path.join(cacheDir, 'module.yaml');
|
||||||
|
|
||||||
|
|
@ -901,7 +878,7 @@ class Manifest {
|
||||||
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
||||||
const moduleConfig = yaml.parse(yamlContent);
|
const moduleConfig = yaml.parse(yamlContent);
|
||||||
return {
|
return {
|
||||||
version: moduleConfig.version || null,
|
version: version || moduleConfig.version || null,
|
||||||
source: 'custom',
|
source: 'custom',
|
||||||
npmPackage: moduleConfig.npmPackage || null,
|
npmPackage: moduleConfig.npmPackage || null,
|
||||||
repoUrl: moduleConfig.repoUrl || null,
|
repoUrl: moduleConfig.repoUrl || null,
|
||||||
|
|
@ -913,13 +890,62 @@ class Manifest {
|
||||||
|
|
||||||
// Unknown module
|
// Unknown module
|
||||||
return {
|
return {
|
||||||
version: null,
|
version,
|
||||||
source: 'unknown',
|
source: 'unknown',
|
||||||
npmPackage: null,
|
npmPackage: null,
|
||||||
repoUrl: null,
|
repoUrl: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read version from .claude-plugin/marketplace.json for a module
|
||||||
|
* @param {string} moduleName - Module code
|
||||||
|
* @returns {string|null} Version or null
|
||||||
|
*/
|
||||||
|
async _readMarketplaceVersion(moduleName, moduleSourcePath = null) {
|
||||||
|
const os = require('node:os');
|
||||||
|
let marketplacePath;
|
||||||
|
|
||||||
|
if (['core', 'bmm'].includes(moduleName)) {
|
||||||
|
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
||||||
|
} else if (moduleSourcePath) {
|
||||||
|
// Walk up from source path to find marketplace.json
|
||||||
|
let dir = moduleSourcePath;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const candidate = path.join(dir, '.claude-plugin', 'marketplace.json');
|
||||||
|
if (await fs.pathExists(candidate)) {
|
||||||
|
marketplacePath = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const parent = path.dirname(dir);
|
||||||
|
if (parent === dir) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to external module cache
|
||||||
|
if (!marketplacePath) {
|
||||||
|
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
|
||||||
|
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await fs.pathExists(marketplacePath)) {
|
||||||
|
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||||
|
const plugins = data?.plugins;
|
||||||
|
if (!Array.isArray(plugins) || plugins.length === 0) return null;
|
||||||
|
let best = null;
|
||||||
|
for (const p of plugins) {
|
||||||
|
if (p.version && (!best || p.version > best)) best = p.version;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch latest version from npm for a package
|
* Fetch latest version from npm for a package
|
||||||
* @param {string} packageName - npm package name
|
* @param {string} packageName - npm package name
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ class ConfigDrivenIdeSetup {
|
||||||
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
|
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
|
||||||
|
|
||||||
// Clean up any old BMAD installation first
|
// Clean up any old BMAD installation first
|
||||||
await this.cleanup(projectDir, options);
|
await this.cleanup(projectDir, options, bmadDir);
|
||||||
|
|
||||||
if (!this.installerConfig) {
|
if (!this.installerConfig) {
|
||||||
return { success: false, reason: 'no-config' };
|
return { success: false, reason: 'no-config' };
|
||||||
|
|
@ -215,15 +215,34 @@ class ConfigDrivenIdeSetup {
|
||||||
* Cleanup IDE configuration
|
* Cleanup IDE configuration
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
*/
|
*/
|
||||||
async cleanup(projectDir, options = {}) {
|
async cleanup(projectDir, options = {}, bmadDir = null) {
|
||||||
|
const resolvedBmadDir = bmadDir || (await this._findBmadDir(projectDir));
|
||||||
|
|
||||||
|
// Build removal set: previously installed skills + removals.txt entries
|
||||||
|
let removalSet;
|
||||||
|
if (options.previousSkillIds && options.previousSkillIds.size > 0) {
|
||||||
|
// Install/update flow: use pre-captured skill IDs (before manifest was overwritten)
|
||||||
|
removalSet = new Set(options.previousSkillIds);
|
||||||
|
if (resolvedBmadDir) {
|
||||||
|
const removals = await this.loadRemovalLists(resolvedBmadDir);
|
||||||
|
for (const entry of removals) removalSet.add(entry);
|
||||||
|
}
|
||||||
|
} else if (resolvedBmadDir) {
|
||||||
|
// Uninstall flow: read from current skill-manifest.csv + removals.txt
|
||||||
|
removalSet = await this._buildUninstallSet(resolvedBmadDir);
|
||||||
|
} else {
|
||||||
|
removalSet = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
|
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
|
||||||
|
// Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
|
||||||
if (this.installerConfig?.legacy_targets) {
|
if (this.installerConfig?.legacy_targets) {
|
||||||
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
||||||
for (const legacyDir of this.installerConfig.legacy_targets) {
|
for (const legacyDir of this.installerConfig.legacy_targets) {
|
||||||
if (this.isGlobalPath(legacyDir)) {
|
if (this.isGlobalPath(legacyDir)) {
|
||||||
await this.warnGlobalLegacy(legacyDir, options);
|
await this.warnGlobalLegacy(legacyDir, options);
|
||||||
} else {
|
} else {
|
||||||
await this.cleanupTarget(projectDir, legacyDir, options);
|
await this.cleanupTarget(projectDir, legacyDir, options, null);
|
||||||
await this.removeEmptyParents(projectDir, legacyDir);
|
await this.removeEmptyParents(projectDir, legacyDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,9 +263,9 @@ class ConfigDrivenIdeSetup {
|
||||||
await this.cleanupRovoDevPrompts(projectDir, options);
|
await this.cleanupRovoDevPrompts(projectDir, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean target directory
|
// Clean current target directory
|
||||||
if (this.installerConfig?.target_dir) {
|
if (this.installerConfig?.target_dir) {
|
||||||
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options);
|
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,23 +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} projectDir - Project directory
|
||||||
* @param {string} targetDir - Target directory to clean
|
* @param {string} targetDir - Target directory to clean
|
||||||
|
* @param {Object} options - Cleanup options
|
||||||
|
* @param {Set<string>|null} removalSet - Entries to remove, or null for legacy prefix matching
|
||||||
*/
|
*/
|
||||||
async cleanupTarget(projectDir, targetDir, options = {}) {
|
async cleanupTarget(projectDir, targetDir, options = {}, removalSet = new Set()) {
|
||||||
const targetPath = path.join(projectDir, targetDir);
|
const targetPath = path.join(projectDir, targetDir);
|
||||||
|
|
||||||
if (!(await fs.pathExists(targetPath))) {
|
if (!(await fs.pathExists(targetPath))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all bmad* files
|
if (removalSet && removalSet.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = await fs.readdir(targetPath);
|
entries = await fs.readdir(targetPath);
|
||||||
} catch {
|
} catch {
|
||||||
// Directory exists but can't be read - skip cleanup
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,23 +426,26 @@ class ConfigDrivenIdeSetup {
|
||||||
let removedCount = 0;
|
let removedCount = 0;
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry || typeof entry !== 'string') {
|
if (!entry || typeof entry !== 'string') continue;
|
||||||
continue;
|
|
||||||
}
|
// Always preserve bmad-os-* utility skills regardless of cleanup mode
|
||||||
if (entry.startsWith('bmad') && !entry.startsWith('bmad-os-')) {
|
if (entry.startsWith('bmad-os-')) continue;
|
||||||
const entryPath = path.join(targetPath, entry);
|
|
||||||
|
// Surgical removal from set, or legacy prefix matching when set is null
|
||||||
|
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
|
||||||
|
|
||||||
|
if (shouldRemove) {
|
||||||
try {
|
try {
|
||||||
await fs.remove(entryPath);
|
await fs.remove(path.join(targetPath, entry));
|
||||||
removedCount++;
|
removedCount++;
|
||||||
} catch {
|
} catch {
|
||||||
// Skip entries that can't be removed (broken symlinks, permission errors)
|
// Skip entries that can't be removed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removedCount > 0 && !options.silent) {
|
// Only log cleanup when it's not a routine reinstall (legacy dir cleanup or actual removals)
|
||||||
await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`);
|
// Suppress for current target_dir since it's always cleaned before a fresh write
|
||||||
}
|
|
||||||
|
|
||||||
// Remove empty directory after cleanup
|
// Remove empty directory after cleanup
|
||||||
if (removedCount > 0) {
|
if (removedCount > 0) {
|
||||||
|
|
@ -339,7 +455,7 @@ class ConfigDrivenIdeSetup {
|
||||||
await fs.remove(targetPath);
|
await fs.remove(targetPath);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Directory may already be gone or in use — skip
|
// Directory may already be gone or in use
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,32 +6,25 @@
|
||||||
startMessage: |
|
startMessage: |
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
🎉 V6 IS HERE! Welcome to BMad Method V6 - Official Stable Release!
|
Agile AI-Driven Development. Powered by BMad Core and a growing module ecosystem.
|
||||||
|
Install official and community modules during setup to customize your experience.
|
||||||
|
|
||||||
The BMad Method is now a Platform powered by the BMad Method Core and Module Ecosystem!
|
🌟 100% free. 100% open source. Always.
|
||||||
- Select and install modules during setup - customize your experience
|
No paywalls. No gated content. Knowledge shared, not sold.
|
||||||
- New BMad Method for Agile AI-Driven Development (the evolution of V4)
|
|
||||||
- Exciting new modules available during installation, with community modules coming soon
|
|
||||||
- Documentation: https://docs.bmad-method.org
|
|
||||||
|
|
||||||
🌟 BMad is 100% free and open source.
|
🌐 CONNECT:
|
||||||
- No gated Discord. No paywalls. No gated content.
|
Website: https://bmadcode.com/
|
||||||
- We believe in empowering everyone, not just those who can pay.
|
Discord: https://discord.gg/gk8jAdXWmj
|
||||||
- Knowledge should be shared, not sold.
|
YouTube: https://www.youtube.com/@BMadCode
|
||||||
|
X: https://x.com/BMadCode
|
||||||
|
Facebook: https://facebook.com/@BMadCode
|
||||||
|
|
||||||
🎤 SPEAKING & MEDIA:
|
⭐ SUPPORT THE PROJECT:
|
||||||
- Available for conferences, podcasts, and media appearances
|
Star us: https://github.com/bmad-code-org/BMAD-METHOD/
|
||||||
- Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
|
Donate: https://buymeacoffee.com/bmad
|
||||||
- For speaking inquiries or interviews, reach out to BMad on Discord!
|
Corporate sponsorship and speaking inquiries: contact@bmadcode.com
|
||||||
|
|
||||||
⭐ HELP US GROW:
|
Docs, blog, and latest updates: https://bmadcode.com/
|
||||||
- Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
|
|
||||||
- Subscribe on YouTube: https://www.youtube.com/@BMadCode
|
|
||||||
- Free Community and Support: https://discord.gg/gk8jAdXWmj
|
|
||||||
- Donate: https://buymeacoffee.com/bmad
|
|
||||||
- Corporate Sponsorship available
|
|
||||||
|
|
||||||
Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/blob/main/CHANGELOG.md
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,50 @@ const fs = require('fs-extra');
|
||||||
const { CLIUtils } = require('./cli-utils');
|
const { CLIUtils } = require('./cli-utils');
|
||||||
const { CustomHandler } = require('./custom-handler');
|
const { CustomHandler } = require('./custom-handler');
|
||||||
const { ExternalModuleManager } = require('./modules/external-manager');
|
const { ExternalModuleManager } = require('./modules/external-manager');
|
||||||
|
const { getProjectRoot } = require('./project-root');
|
||||||
const prompts = require('./prompts');
|
const prompts = require('./prompts');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read module version from .claude-plugin/marketplace.json
|
||||||
|
* @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis')
|
||||||
|
* @returns {string} Version string or empty string
|
||||||
|
*/
|
||||||
|
async function getMarketplaceVersion(moduleCode) {
|
||||||
|
let marketplacePath;
|
||||||
|
if (moduleCode === 'core' || moduleCode === 'bmm') {
|
||||||
|
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
||||||
|
} else {
|
||||||
|
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleCode);
|
||||||
|
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (await fs.pathExists(marketplacePath)) {
|
||||||
|
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||||
|
return _extractMarketplaceVersion(data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the highest version from marketplace.json plugins array.
|
||||||
|
* Handles multiple plugins per file safely.
|
||||||
|
* @param {Object} data - Parsed marketplace.json
|
||||||
|
* @returns {string} Version string or empty string
|
||||||
|
*/
|
||||||
|
function _extractMarketplaceVersion(data) {
|
||||||
|
const plugins = data?.plugins;
|
||||||
|
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
||||||
|
// Use the highest version across all plugins in the file
|
||||||
|
let best = '';
|
||||||
|
for (const p of plugins) {
|
||||||
|
if (p.version && (!best || p.version > best)) best = p.version;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
// Separator class for visual grouping in select/multiselect prompts
|
// Separator class for visual grouping in select/multiselect prompts
|
||||||
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
||||||
class Separator {
|
class Separator {
|
||||||
|
|
@ -70,17 +112,14 @@ class UI {
|
||||||
if (hasExistingInstall) {
|
if (hasExistingInstall) {
|
||||||
// Get version information
|
// Get version information
|
||||||
const { existingInstall, bmadDir } = await this.getExistingInstallation(confirmedDirectory);
|
const { existingInstall, bmadDir } = await this.getExistingInstallation(confirmedDirectory);
|
||||||
const packageJsonPath = path.join(__dirname, '../../package.json');
|
|
||||||
const currentVersion = require(packageJsonPath).version;
|
|
||||||
const installedVersion = existingInstall.installed ? existingInstall.version || 'unknown' : 'unknown';
|
|
||||||
|
|
||||||
// Build menu choices dynamically
|
// Build menu choices dynamically
|
||||||
const choices = [];
|
const choices = [];
|
||||||
|
|
||||||
// Always show Quick Update first (allows refreshing installation even on same version)
|
// Always show Quick Update first (allows refreshing installation even on same version)
|
||||||
if (installedVersion !== 'unknown') {
|
if (existingInstall.installed) {
|
||||||
choices.push({
|
choices.push({
|
||||||
name: `Quick Update (v${installedVersion} → v${currentVersion})`,
|
name: 'Quick Update',
|
||||||
value: 'quick-update',
|
value: 'quick-update',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -880,14 +919,18 @@ class UI {
|
||||||
const lockedValues = ['core'];
|
const lockedValues = ['core'];
|
||||||
|
|
||||||
// Core module is always installed — show it locked at the top
|
// Core module is always installed — show it locked at the top
|
||||||
allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' });
|
const coreVersion = await getMarketplaceVersion('core');
|
||||||
|
const coreLabel = coreVersion ? `BMad Core Module (v${coreVersion})` : 'BMad Core Module';
|
||||||
|
allOptions.push({ label: coreLabel, value: 'core', hint: 'Core configuration and shared resources' });
|
||||||
initialValues.push('core');
|
initialValues.push('core');
|
||||||
|
|
||||||
// Helper to build module entry with proper sorting and selection
|
// Helper to build module entry with proper sorting and selection
|
||||||
const buildModuleEntry = (mod, value, group) => {
|
const buildModuleEntry = async (mod, value, group) => {
|
||||||
const isInstalled = installedModuleIds.has(value);
|
const isInstalled = installedModuleIds.has(value);
|
||||||
|
const version = await getMarketplaceVersion(value);
|
||||||
|
const label = version ? `${mod.name} (v${version})` : mod.name;
|
||||||
return {
|
return {
|
||||||
label: mod.name,
|
label,
|
||||||
value,
|
value,
|
||||||
hint: mod.description || group,
|
hint: mod.description || group,
|
||||||
// Pre-select only if already installed (not on fresh install)
|
// Pre-select only if already installed (not on fresh install)
|
||||||
|
|
@ -899,7 +942,7 @@ class UI {
|
||||||
const localEntries = [];
|
const localEntries = [];
|
||||||
for (const mod of localModules) {
|
for (const mod of localModules) {
|
||||||
if (!mod.isCustom && mod.id !== 'core') {
|
if (!mod.isCustom && mod.id !== 'core') {
|
||||||
const entry = buildModuleEntry(mod, mod.id, 'Local');
|
const entry = await buildModuleEntry(mod, mod.id, 'Local');
|
||||||
localEntries.push(entry);
|
localEntries.push(entry);
|
||||||
if (entry.selected) {
|
if (entry.selected) {
|
||||||
initialValues.push(mod.id);
|
initialValues.push(mod.id);
|
||||||
|
|
@ -912,7 +955,7 @@ class UI {
|
||||||
const officialModules = [];
|
const officialModules = [];
|
||||||
for (const mod of externalModules) {
|
for (const mod of externalModules) {
|
||||||
if (mod.type === 'bmad-org') {
|
if (mod.type === 'bmad-org') {
|
||||||
const entry = buildModuleEntry(mod, mod.code, 'Official');
|
const entry = await buildModuleEntry(mod, mod.code, 'Official');
|
||||||
officialModules.push(entry);
|
officialModules.push(entry);
|
||||||
if (entry.selected) {
|
if (entry.selected) {
|
||||||
initialValues.push(mod.code);
|
initialValues.push(mod.code);
|
||||||
|
|
@ -925,7 +968,7 @@ class UI {
|
||||||
const communityModules = [];
|
const communityModules = [];
|
||||||
for (const mod of externalModules) {
|
for (const mod of externalModules) {
|
||||||
if (mod.type === 'community') {
|
if (mod.type === 'community') {
|
||||||
const entry = buildModuleEntry(mod, mod.code, 'Community');
|
const entry = await buildModuleEntry(mod, mod.code, 'Community');
|
||||||
communityModules.push(entry);
|
communityModules.push(entry);
|
||||||
if (entry.selected) {
|
if (entry.selected) {
|
||||||
initialValues.push(mod.code);
|
initialValues.push(mod.code);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue