diff --git a/removals.txt b/removals.txt new file mode 100644 index 000000000..81a2b5dce --- /dev/null +++ b/removals.txt @@ -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 diff --git a/test/test-installation-components.js b/test/test-installation-components.js index b548cbabe..1ac4b386d 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -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(); diff --git a/tools/installer/cli-utils.js b/tools/installer/cli-utils.js index 6ca615534..a0efdbe06 100644 --- a/tools/installer/cli-utils.js +++ b/tools/installer/cli-utils.js @@ -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, diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index a0ea9a66e..14bb2ac9c 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -26,6 +26,31 @@ 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 data.plugins?.[0]?.version || ''; + } catch { + return ''; + } + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return ''; + } + /** * Main installation method * @param {Object} config - Installation configuration @@ -52,9 +77,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'); + try { + if (await fs.pathExists(prevCsvPath)) { + 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 { + // No previous manifest - fresh install + } await this._cacheCustomModules(paths, addResult); @@ -65,7 +117,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 +128,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 +374,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 +389,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 +610,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 +624,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 +657,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 +1125,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 +1138,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 +1175,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 +1296,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; diff --git a/tools/installer/core/manifest.js b/tools/installer/core/manifest.js index d6eade648..0fa350279 100644 --- a/tools/installer/core/manifest.js +++ b/tools/installer/core/manifest.js @@ -840,11 +840,13 @@ class Manifest { const os = require('node:os'); const yaml = require('yaml'); - // Built-in modules use BMad version (only core and bmm are in BMAD-METHOD repo) + // All module versions come from .claude-plugin/marketplace.json + const version = await this._readMarketplaceVersion(moduleName); + + // Determine source type if (['core', 'bmm'].includes(moduleName)) { - const bmadVersion = require(path.join(getProjectRoot(), 'package.json')).version; return { - version: bmadVersion, + version, source: 'built-in', npmPackage: null, repoUrl: null, @@ -857,35 +859,8 @@ 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}`); - } - } - } - return { - version: version, + version, source: 'external', npmPackage: moduleInfo.npmPackage || null, repoUrl: moduleInfo.url || null, @@ -901,7 +876,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 +888,40 @@ 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) { + const os = require('node:os'); + let marketplacePath; + + if (['core', 'bmm'].includes(moduleName)) { + marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json'); + } else { + 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')); + return data.plugins?.[0]?.version || null; + } + } catch { + // ignore + } + return null; + } + /** * Fetch latest version from npm for a package * @param {string} packageName - npm package name diff --git a/tools/installer/ide/_config-driven.js b/tools/installer/ide/_config-driven.js index 603ffc7a4..6bb48af2d 100644 --- a/tools/installer/ide/_config-driven.js +++ b/tools/installer/ide/_config-driven.js @@ -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} 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} 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} 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|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,23 @@ 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; + + // Surgical removal from set, or legacy prefix matching when set is null + const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad') && !entry.startsWith('bmad-os-'); + + 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 +452,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 } } } diff --git a/tools/installer/install-messages.yaml b/tools/installer/install-messages.yaml index 0fc32cc82..4aff87a95 100644 --- a/tools/installer/install-messages.yaml +++ b/tools/installer/install-messages.yaml @@ -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/ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/tools/installer/ui.js b/tools/installer/ui.js index 03d38e4da..85f9d07c9 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -4,8 +4,33 @@ 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 data.plugins?.[0]?.version || ''; + } + } catch { + // ignore + } + return ''; +} + // 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 +95,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 +902,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 +925,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 +938,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 +951,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);