feat: replace installer spinner with tasks() progress and consolidate summary

Replace the serial spinner during non-interactive install phases with
@clack/prompts tasks() component for clearer progress visibility. The
install flow now uses two tasks() blocks (pre-IDE and post-IDE) with
the IDE setup retaining its own spinner since it may prompt.

- Refactor install phases into tasks() callbacks with message() updates
- Merge next-steps content into the "BMAD is ready to use!" summary note
- Fix spinner.stop() tense: "Reviewing..." → past tense ("reviewed")
- Move directory creation output after tasks() to avoid breaking progress
- Remove dead showInstallSummary() from ui.js
- Harden error handling: try/finally on IDE spinner, safe catch block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Davor Racić 2026-02-09 12:20:49 +01:00
parent da37069538
commit 65b5afb9f1
2 changed files with 370 additions and 356 deletions

View File

@ -452,7 +452,7 @@ class Installer {
spinner.start('Preparing update...'); spinner.start('Preparing update...');
} else { } else {
if (spinner.isSpinning) { if (spinner.isSpinning) {
spinner.stop('Reviewing module changes'); spinner.stop('Module changes reviewed');
} }
await prompts.log.warn('Modules to be removed:'); await prompts.log.warn('Modules to be removed:');
@ -733,7 +733,7 @@ class Installer {
} }
} else { } else {
if (spinner.isSpinning) { if (spinner.isSpinning) {
spinner.stop('Reviewing IDE changes'); spinner.stop('IDE changes reviewed');
} }
await prompts.log.warn('IDEs to be removed:'); await prompts.log.warn('IDEs to be removed:');
@ -784,9 +784,9 @@ class Installer {
const addResult = (step, status, detail = '') => results.push({ step, status, detail }); const addResult = (step, status, detail = '') => results.push({ step, status, detail });
if (spinner.isSpinning) { if (spinner.isSpinning) {
spinner.message('Installing...'); spinner.message('Preparing installation...');
} else { } else {
spinner.start('Installing...'); spinner.start('Preparing installation...');
} }
// Create bmad directory structure // Create bmad directory structure
@ -815,20 +815,10 @@ class Installer {
const projectRoot = getProjectRoot(); const projectRoot = getProjectRoot();
// Step 1: Install core module first (if requested)
if (config.installCore) {
spinner.message('Installing BMAD core...');
await this.installCoreWithDependencies(bmadDir, { core: {} });
addResult('Core', 'ok', 'installed');
// Generate core config file
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
}
// Custom content is already handled in UI before module selection // Custom content is already handled in UI before module selection
let finalCustomContent = config.customContent; const finalCustomContent = config.customContent;
// Step 3: Prepare modules list including cached custom modules // Prepare modules list including cached custom modules
let allModules = [...(config.modules || [])]; let allModules = [...(config.modules || [])];
// During quick update, we might have custom module sources from the manifest // During quick update, we might have custom module sources from the manifest
@ -867,8 +857,6 @@ class Installer {
allModules = allModules.filter((m) => m !== 'core'); allModules = allModules.filter((m) => m !== 'core');
} }
const modulesToInstall = allModules;
// For dependency resolution, we only need regular modules (not custom modules) // For dependency resolution, we only need regular modules (not custom modules)
// Custom modules are already installed in _bmad and don't need dependency resolution from source // Custom modules are already installed in _bmad and don't need dependency resolution from source
const regularModulesForResolution = allModules.filter((module) => { const regularModulesForResolution = allModules.filter((module) => {
@ -883,70 +871,88 @@ class Installer {
return !isCustom; return !isCustom;
}); });
// For dependency resolution, we need to pass the project root // Stop spinner before tasks() takes over progress display
spinner.stop('Preparation complete');
// ─────────────────────────────────────────────────────────────────────────
// FIRST TASKS BLOCK: Core installation through manifests (non-interactive)
// ─────────────────────────────────────────────────────────────────────────
const isQuickUpdate = config._quickUpdate || false;
// Shared resolution result across task callbacks (closure-scoped, not on `this`)
let taskResolution;
// Build task list conditionally
const installTasks = [];
// Core installation task
if (config.installCore) {
installTasks.push({
title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core',
task: async (message) => {
await this.installCoreWithDependencies(bmadDir, { core: {} });
addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed');
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
return isQuickUpdate ? 'Core updated' : 'Core installed';
},
});
}
// Dependency resolution task
installTasks.push({
title: 'Resolving dependencies',
task: async (message) => {
// Create a temporary module manager that knows about custom content locations // Create a temporary module manager that knows about custom content locations
const tempModuleManager = new ModuleManager({ const tempModuleManager = new ModuleManager({
bmadDir: bmadDir, // Pass bmadDir so we can check cache bmadDir: bmadDir,
}); });
spinner.message('Resolving dependencies...'); taskResolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
verbose: config.verbose, verbose: config.verbose,
moduleManager: tempModuleManager, moduleManager: tempModuleManager,
}); });
return 'Dependencies resolved';
},
});
// Install modules with their dependencies // Module installation task
if (allModules && allModules.length > 0) { if (allModules && allModules.length > 0) {
installTasks.push({
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
task: async (message) => {
const resolution = taskResolution;
const installedModuleNames = new Set(); const installedModuleNames = new Set();
for (const moduleName of allModules) { for (const moduleName of allModules) {
// Skip if already installed if (installedModuleNames.has(moduleName)) continue;
if (installedModuleNames.has(moduleName)) {
continue;
}
installedModuleNames.add(moduleName); installedModuleNames.add(moduleName);
// Show appropriate message based on whether this is a quick update message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
const isQuickUpdate = config._quickUpdate || false;
spinner.message(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`);
// Check if this is a custom module // Check if this is a custom module
let isCustomModule = false; let isCustomModule = false;
let customInfo = null; let customInfo = null;
let useCache = false;
// First check if we have a cached version // First check if we have a cached version
if (finalCustomContent && finalCustomContent.cachedModules) { if (finalCustomContent && finalCustomContent.cachedModules) {
const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName); const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
if (cachedModule) { if (cachedModule) {
isCustomModule = true; isCustomModule = true;
customInfo = { customInfo = { id: moduleName, path: cachedModule.cachePath, config: {} };
id: moduleName,
path: cachedModule.cachePath,
config: {},
};
useCache = true;
} }
} }
// Then check if we have custom module sources from the manifest (for quick update) // Then check custom module sources from manifest (for quick update)
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) { if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
customInfo = config._customModuleSources.get(moduleName); customInfo = config._customModuleSources.get(moduleName);
isCustomModule = true; isCustomModule = true;
// Check if this is a cached module (source path starts with _config)
if ( if (
customInfo.sourcePath && customInfo.sourcePath &&
(customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom')) (customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom')) &&
) { !customInfo.path
useCache = true; )
// Make sure we have the right path structure
if (!customInfo.path) {
customInfo.path = customInfo.sourcePath; customInfo.path = customInfo.sourcePath;
} }
}
}
// Finally check regular custom content // Finally check regular custom content
if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) { if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
@ -962,16 +968,12 @@ class Installer {
} }
if (isCustomModule && customInfo) { if (isCustomModule && customInfo) {
// Custom modules are now installed via ModuleManager just like standard modules
// The custom module path should already be in customModulePaths from earlier setup
if (!customModulePaths.has(moduleName) && customInfo.path) { if (!customModulePaths.has(moduleName) && customInfo.path) {
customModulePaths.set(moduleName, customInfo.path); customModulePaths.set(moduleName, customInfo.path);
this.moduleManager.setCustomModulePaths(customModulePaths); this.moduleManager.setCustomModulePaths(customModulePaths);
} }
const collectedModuleConfig = moduleConfigs[moduleName] || {}; const collectedModuleConfig = moduleConfigs[moduleName] || {};
// Use ModuleManager to install the custom module
await this.moduleManager.install( await this.moduleManager.install(
moduleName, moduleName,
bmadDir, bmadDir,
@ -981,19 +983,15 @@ class Installer {
{ {
isCustom: true, isCustom: true,
moduleConfig: collectedModuleConfig, moduleConfig: collectedModuleConfig,
isQuickUpdate: config._quickUpdate || false, isQuickUpdate: isQuickUpdate,
installer: this, installer: this,
silent: true, silent: true,
}, },
); );
// Create module config (include collected config from module.yaml prompts)
await this.generateModuleConfigs(bmadDir, { await this.generateModuleConfigs(bmadDir, {
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig }, [moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
}); });
} else { } else {
// Regular module installation
// Special case for core module
if (moduleName === 'core') { if (moduleName === 'core') {
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]); await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
} else { } else {
@ -1015,102 +1013,101 @@ class Installer {
files.data.length + files.data.length +
files.other.length; files.other.length;
if (totalFiles > 0) { if (totalFiles > 0) {
spinner.message(`Installing ${module} dependencies...`); message(`Installing ${module} dependencies...`);
await this.installPartialModule(module, bmadDir, files); await this.installPartialModule(module, bmadDir, files);
} }
} }
} }
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
},
});
} }
// All content is now installed as modules - no separate custom content handling needed // Configuration generation task
installTasks.push({
title: 'Generating configurations',
task: async (message) => {
// Generate clean config.yaml files for each installed module // Generate clean config.yaml files for each installed module
spinner.message('Generating module configurations...');
await this.generateModuleConfigs(bmadDir, moduleConfigs); await this.generateModuleConfigs(bmadDir, moduleConfigs);
addResult('Configurations', 'ok', 'generated'); addResult('Configurations', 'ok', 'generated');
// Create agent configuration files // Pre-register manifest files
// Note: Legacy createAgentConfigs removed - using YAML customize system instead
// Customize templates are now created in processAgentFiles when building YAML agents
// Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion)
const cfgDir = path.join(bmadDir, '_config'); const cfgDir = path.join(bmadDir, '_config');
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml')); this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv')); this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv')); this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv')); this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup // Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes
// This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv // This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
spinner.message('Generating workflow and agent manifests...'); message('Generating manifests...');
const manifestGen = new ManifestGenerator(); const manifestGen = new ManifestGenerator();
// For quick update, we need ALL installed modules in the manifest
// Not just the ones being updated
const allModulesForManifest = config._quickUpdate const allModulesForManifest = config._quickUpdate
? config._existingModules || allModules || [] ? config._existingModules || allModules || []
: config._preserveModules : config._preserveModules
? [...allModules, ...config._preserveModules] ? [...allModules, ...config._preserveModules]
: allModules || []; : allModules || [];
// For regular installs (including when called from quick update), use what we have
let modulesForCsvPreserve; let modulesForCsvPreserve;
if (config._quickUpdate) { if (config._quickUpdate) {
// Quick update - use existing modules or fall back to modules being updated
modulesForCsvPreserve = config._existingModules || allModules || []; modulesForCsvPreserve = config._existingModules || allModules || [];
} else { } else {
// Regular install - use the modules we're installing plus any preserved ones
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules; modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
} }
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], { const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
ides: config.ides || [], ides: config.ides || [],
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir preservedModules: modulesForCsvPreserve,
}); });
// Custom modules are now included in the main modules list - no separate tracking needed
addResult( addResult(
'Manifests', 'Manifests',
'ok', 'ok',
`${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`, `${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`,
); );
// Merge all module-help.csv files into bmad-help.csv // Merge help catalogs
// This must happen AFTER generateManifests because it depends on agent-manifest.csv message('Generating help catalog...');
spinner.message('Generating workflow help catalog...');
await this.mergeModuleHelpCatalogs(bmadDir); await this.mergeModuleHelpCatalogs(bmadDir);
addResult('Help catalog', 'ok'); addResult('Help catalog', 'ok');
// Configure IDEs and copy documentation return 'Configurations generated';
if (!config.skipIde && config.ides && config.ides.length > 0) { },
// Ensure IDE manager is initialized (handlers may not be loaded in quick update flow) });
await this.ideManager.ensureInitialized();
// Filter out any undefined/null values from the IDE list await prompts.tasks(installTasks);
// Resolution is now available via closure-scoped taskResolution
const resolution = taskResolution;
// ─────────────────────────────────────────────────────────────────────────
// IDE SETUP: Keep as spinner since it may prompt for user input
// ─────────────────────────────────────────────────────────────────────────
if (!config.skipIde && config.ides && config.ides.length > 0) {
await this.ideManager.ensureInitialized();
const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string'); const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
if (validIdes.length === 0) { if (validIdes.length === 0) {
addResult('IDE configuration', 'warn', 'no valid IDEs selected'); addResult('IDE configuration', 'warn', 'no valid IDEs selected');
} else { } else {
// Check if any IDE might need prompting (no pre-collected config)
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]); const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
const ideSpinner = await prompts.spinner();
ideSpinner.start('Configuring IDEs...');
try {
for (const ide of validIdes) { for (const ide of validIdes) {
if (!needsPrompting || ideConfigurations[ide]) { if (!needsPrompting || ideConfigurations[ide]) {
// All IDEs pre-configured, or this specific IDE has config: keep spinner running ideSpinner.message(`Configuring ${ide}...`);
spinner.message(`Configuring ${ide}...`);
} else { } else {
// This IDE needs prompting: stop spinner to allow user interaction if (ideSpinner.isSpinning) {
if (spinner.isSpinning) { ideSpinner.stop('Ready for IDE configuration');
spinner.stop('Ready for IDE configuration');
} }
} }
// Silent when this IDE has pre-collected config (no prompts for THIS IDE)
const ideHasConfig = Boolean(ideConfigurations[ide]);
// Suppress stray console output for pre-configured IDEs (no user interaction) // Suppress stray console output for pre-configured IDEs (no user interaction)
const ideHasConfig = Boolean(ideConfigurations[ide]);
const originalLog = console.log; const originalLog = console.log;
if (!config.verbose && ideHasConfig) { if (!config.verbose && ideHasConfig) {
console.log = () => {}; console.log = () => {};
@ -1123,12 +1120,10 @@ class Installer {
silent: ideHasConfig, silent: ideHasConfig,
}); });
// Save IDE configuration for future updates
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) { if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
} }
// Collect result for summary
if (setupResult.success) { if (setupResult.success) {
addResult(ide, 'ok', setupResult.detail || ''); addResult(ide, 'ok', setupResult.detail || '');
} else { } else {
@ -1138,18 +1133,30 @@ class Installer {
console.log = originalLog; console.log = originalLog;
} }
// Restart spinner if we stopped it for prompting if (needsPrompting && !ideSpinner.isSpinning) {
if (needsPrompting && !spinner.isSpinning) { ideSpinner.start('Configuring IDEs...');
spinner.start('Configuring IDEs...'); }
}
} finally {
if (ideSpinner.isSpinning) {
ideSpinner.stop('IDE configuration complete');
} }
} }
} }
} }
// Run module-specific installers after IDE setup // ─────────────────────────────────────────────────────────────────────────
spinner.message('Running module-specific installers...'); // SECOND TASKS BLOCK: Post-IDE operations (non-interactive)
// ─────────────────────────────────────────────────────────────────────────
const postIdeTasks = [];
// Create a conditional logger based on verbose mode // Collect directory creation results for output after tasks() completes
const dirResults = { createdDirs: [], createdWdsFolders: [] };
// Module directory creation task
postIdeTasks.push({
title: 'Running module installers',
task: async (message) => {
const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose; const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
const moduleLogger = { const moduleLogger = {
log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined), log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined),
@ -1157,11 +1164,6 @@ class Installer {
warn: async (msg) => await prompts.log.warn(msg), warn: async (msg) => await prompts.log.warn(msg),
}; };
// Create directories for all modules
spinner.message('Creating module directories...');
const allCreatedDirs = [];
const allCreatedWdsFolders = [];
// Core module directories // Core module directories
if (config.installCore || resolution.byModule.core) { if (config.installCore || resolution.byModule.core) {
const result = await this.moduleManager.createModuleDirectories('core', bmadDir, { const result = await this.moduleManager.createModuleDirectories('core', bmadDir, {
@ -1172,14 +1174,15 @@ class Installer {
silent: true, silent: true,
}); });
if (result) { if (result) {
allCreatedDirs.push(...result.createdDirs); dirResults.createdDirs.push(...result.createdDirs);
allCreatedWdsFolders.push(...result.createdWdsFolders); dirResults.createdWdsFolders.push(...result.createdWdsFolders);
} }
} }
// User-selected module directories // User-selected module directories
if (config.modules && config.modules.length > 0) { if (config.modules && config.modules.length > 0) {
for (const moduleName of config.modules) { for (const moduleName of config.modules) {
message(`Setting up ${moduleName}...`);
const result = await this.moduleManager.createModuleDirectories(moduleName, bmadDir, { const result = await this.moduleManager.createModuleDirectories(moduleName, bmadDir, {
installedIDEs: config.ides || [], installedIDEs: config.ides || [],
moduleConfig: moduleConfigs[moduleName] || {}, moduleConfig: moduleConfigs[moduleName] || {},
@ -1188,35 +1191,30 @@ class Installer {
silent: true, silent: true,
}); });
if (result) { if (result) {
allCreatedDirs.push(...result.createdDirs); dirResults.createdDirs.push(...result.createdDirs);
allCreatedWdsFolders.push(...result.createdWdsFolders); dirResults.createdWdsFolders.push(...result.createdWdsFolders);
} }
} }
} }
// Batch output: single log message for all created directories across all modules
if (allCreatedDirs.length > 0) {
const color = await prompts.getColor();
const lines = allCreatedDirs.map((d) => ` ${d}`).join('\n');
await prompts.log.message(color.yellow(`Created directories:\n${lines}`));
}
if (allCreatedWdsFolders.length > 0) {
const color = await prompts.getColor();
const lines = allCreatedWdsFolders.map((f) => color.dim(`${f}/`)).join('\n');
await prompts.log.message(color.cyan(`Created WDS folder structure:\n${lines}`));
}
addResult('Module installers', 'ok'); addResult('Module installers', 'ok');
return 'Module setup complete';
},
});
// Note: Manifest files are already created by ManifestGenerator above // File restoration task (only for updates)
// No need to create legacy manifest.csv anymore if (
config._isUpdate &&
// If this was an update, restore custom files ((config._customFiles && config._customFiles.length > 0) || (config._modifiedFiles && config._modifiedFiles.length > 0))
) {
postIdeTasks.push({
title: 'Finalizing installation',
task: async (message) => {
let customFiles = []; let customFiles = [];
let modifiedFiles = []; let modifiedFiles = [];
if (config._isUpdate) {
if (config._customFiles && config._customFiles.length > 0) { if (config._customFiles && config._customFiles.length > 0) {
spinner.message(`Restoring ${config._customFiles.length} custom files...`); message(`Restoring ${config._customFiles.length} custom files...`);
for (const originalPath of config._customFiles) { for (const originalPath of config._customFiles) {
const relativePath = path.relative(bmadDir, originalPath); const relativePath = path.relative(bmadDir, originalPath);
@ -1228,7 +1226,6 @@ class Installer {
} }
} }
// Clean up temp backup
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) { if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
await fs.remove(config._tempBackupDir); await fs.remove(config._tempBackupDir);
} }
@ -1239,9 +1236,8 @@ class Installer {
if (config._modifiedFiles && config._modifiedFiles.length > 0) { if (config._modifiedFiles && config._modifiedFiles.length > 0) {
modifiedFiles = config._modifiedFiles; modifiedFiles = config._modifiedFiles;
// Restore modified files as .bak files
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) { if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
spinner.message(`Restoring ${modifiedFiles.length} modified files as .bak...`); message(`Restoring ${modifiedFiles.length} modified files as .bak...`);
for (const modifiedFile of modifiedFiles) { for (const modifiedFile of modifiedFiles) {
const relativePath = path.relative(bmadDir, modifiedFile.path); const relativePath = path.relative(bmadDir, modifiedFile.path);
@ -1254,17 +1250,36 @@ class Installer {
} }
} }
// Clean up temp backup
await fs.remove(config._tempModifiedBackupDir); await fs.remove(config._tempModifiedBackupDir);
} }
} }
// Store for summary access
config._restoredCustomFiles = customFiles;
config._restoredModifiedFiles = modifiedFiles;
return 'Installation finalized';
},
});
} }
// Blank line for spacing before final status await prompts.tasks(postIdeTasks);
await prompts.log.message('');
// Stop the single installation spinner // Render directory creation output after tasks() to avoid breaking progress display
spinner.stop('Installation complete'); if (dirResults.createdDirs.length > 0) {
const color = await prompts.getColor();
const lines = dirResults.createdDirs.map((d) => ` ${d}`).join('\n');
await prompts.log.message(color.yellow(`Created directories:\n${lines}`));
}
if (dirResults.createdWdsFolders.length > 0) {
const color = await prompts.getColor();
const lines = dirResults.createdWdsFolders.map((f) => color.dim(`${f}/`)).join('\n');
await prompts.log.message(color.cyan(`Created WDS folder structure:\n${lines}`));
}
// Retrieve restored file info for summary
const customFiles = config._restoredCustomFiles || [];
const modifiedFiles = config._restoredModifiedFiles || [];
// Render consolidated summary // Render consolidated summary
await this.renderInstallSummary(results, { await this.renderInstallSummary(results, {
@ -1283,7 +1298,15 @@ class Installer {
projectDir: projectDir, projectDir: projectDir,
}; };
} catch (error) { } catch (error) {
try {
if (spinner.isSpinning) {
spinner.error('Installation failed'); spinner.error('Installation failed');
} else {
await prompts.log.error('Installation failed');
}
} catch {
// Ensure the original error is never swallowed by a logging failure
}
throw error; throw error;
} }
} }
@ -1323,6 +1346,9 @@ class Installer {
lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`); lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`);
} }
// Next steps
lines.push('', ` Docs: ${color.dim('https://docs.bmad-method.org/')}`, ` Run ${color.cyan('/bmad-help')} in your IDE to get started`);
await prompts.note(lines.join('\n'), 'BMAD is ready to use!'); await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
} }

View File

@ -691,18 +691,6 @@ class UI {
}); });
} }
/**
* Display installation summary
* @param {Object} result - Installation result
*/
async showInstallSummary(result) {
let summary = `Installed to: ${result.path}`;
if (result.modules && result.modules.length > 0) {
summary += `\nModules: ${result.modules.join(', ')}`;
}
await prompts.note(summary, 'BMAD is ready to use!');
}
/** /**
* Get confirmed directory from user * Get confirmed directory from user
* @returns {string} Confirmed directory path * @returns {string} Confirmed directory path