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:
parent
da37069538
commit
65b5afb9f1
|
|
@ -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,388 +871,415 @@ class Installer {
|
||||||
return !isCustom;
|
return !isCustom;
|
||||||
});
|
});
|
||||||
|
|
||||||
// For dependency resolution, we need to pass the project root
|
// Stop spinner before tasks() takes over progress display
|
||||||
// Create a temporary module manager that knows about custom content locations
|
spinner.stop('Preparation complete');
|
||||||
const tempModuleManager = new ModuleManager({
|
|
||||||
bmadDir: bmadDir, // Pass bmadDir so we can check cache
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// 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
|
||||||
|
const tempModuleManager = new ModuleManager({
|
||||||
|
bmadDir: bmadDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
taskResolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
|
||||||
|
verbose: config.verbose,
|
||||||
|
moduleManager: tempModuleManager,
|
||||||
|
});
|
||||||
|
return 'Dependencies resolved';
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
spinner.message('Resolving dependencies...');
|
// Module installation task
|
||||||
|
|
||||||
const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
|
|
||||||
verbose: config.verbose,
|
|
||||||
moduleManager: tempModuleManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Install modules with their dependencies
|
|
||||||
if (allModules && allModules.length > 0) {
|
if (allModules && allModules.length > 0) {
|
||||||
const installedModuleNames = new Set();
|
installTasks.push({
|
||||||
|
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
|
||||||
|
task: async (message) => {
|
||||||
|
const resolution = taskResolution;
|
||||||
|
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)) {
|
installedModuleNames.add(moduleName);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
|
|
||||||
customInfo = config._customModuleSources.get(moduleName);
|
|
||||||
isCustomModule = true;
|
|
||||||
|
|
||||||
// Check if this is a cached module (source path starts with _config)
|
|
||||||
if (
|
|
||||||
customInfo.sourcePath &&
|
|
||||||
(customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom'))
|
|
||||||
) {
|
|
||||||
useCache = true;
|
|
||||||
// Make sure we have the right path structure
|
|
||||||
if (!customInfo.path) {
|
|
||||||
customInfo.path = customInfo.sourcePath;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally check regular custom content
|
// Then check custom module sources from manifest (for quick update)
|
||||||
if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
|
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
|
||||||
const customHandler = new CustomHandler();
|
customInfo = config._customModuleSources.get(moduleName);
|
||||||
for (const customFile of finalCustomContent.selectedFiles) {
|
|
||||||
const info = await customHandler.getCustomInfo(customFile, projectDir);
|
|
||||||
if (info && info.id === moduleName) {
|
|
||||||
isCustomModule = true;
|
isCustomModule = true;
|
||||||
customInfo = info;
|
if (
|
||||||
break;
|
customInfo.sourcePath &&
|
||||||
|
(customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom')) &&
|
||||||
|
!customInfo.path
|
||||||
|
)
|
||||||
|
customInfo.path = customInfo.sourcePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally check regular custom content
|
||||||
|
if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
|
||||||
|
const customHandler = new CustomHandler();
|
||||||
|
for (const customFile of finalCustomContent.selectedFiles) {
|
||||||
|
const info = await customHandler.getCustomInfo(customFile, projectDir);
|
||||||
|
if (info && info.id === moduleName) {
|
||||||
|
isCustomModule = true;
|
||||||
|
customInfo = info;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCustomModule && customInfo) {
|
||||||
|
if (!customModulePaths.has(moduleName) && customInfo.path) {
|
||||||
|
customModulePaths.set(moduleName, customInfo.path);
|
||||||
|
this.moduleManager.setCustomModulePaths(customModulePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectedModuleConfig = moduleConfigs[moduleName] || {};
|
||||||
|
await this.moduleManager.install(
|
||||||
|
moduleName,
|
||||||
|
bmadDir,
|
||||||
|
(filePath) => {
|
||||||
|
this.installedFiles.add(filePath);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCustom: true,
|
||||||
|
moduleConfig: collectedModuleConfig,
|
||||||
|
isQuickUpdate: isQuickUpdate,
|
||||||
|
installer: this,
|
||||||
|
silent: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await this.generateModuleConfigs(bmadDir, {
|
||||||
|
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (moduleName === 'core') {
|
||||||
|
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
|
||||||
|
} else {
|
||||||
|
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install partial modules (only dependencies)
|
||||||
|
for (const [module, files] of Object.entries(resolution.byModule)) {
|
||||||
|
if (!allModules.includes(module) && module !== 'core') {
|
||||||
|
const totalFiles =
|
||||||
|
files.agents.length +
|
||||||
|
files.tasks.length +
|
||||||
|
files.tools.length +
|
||||||
|
files.templates.length +
|
||||||
|
files.data.length +
|
||||||
|
files.other.length;
|
||||||
|
if (totalFiles > 0) {
|
||||||
|
message(`Installing ${module} dependencies...`);
|
||||||
|
await this.installPartialModule(module, bmadDir, files);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isCustomModule && customInfo) {
|
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
|
||||||
// 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) {
|
}
|
||||||
customModulePaths.set(moduleName, customInfo.path);
|
|
||||||
this.moduleManager.setCustomModulePaths(customModulePaths);
|
|
||||||
}
|
|
||||||
|
|
||||||
const collectedModuleConfig = moduleConfigs[moduleName] || {};
|
// Configuration generation task
|
||||||
|
installTasks.push({
|
||||||
|
title: 'Generating configurations',
|
||||||
|
task: async (message) => {
|
||||||
|
// Generate clean config.yaml files for each installed module
|
||||||
|
await this.generateModuleConfigs(bmadDir, moduleConfigs);
|
||||||
|
addResult('Configurations', 'ok', 'generated');
|
||||||
|
|
||||||
// Use ModuleManager to install the custom module
|
// Pre-register manifest files
|
||||||
await this.moduleManager.install(
|
const cfgDir = path.join(bmadDir, '_config');
|
||||||
moduleName,
|
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
|
||||||
bmadDir,
|
this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
|
||||||
(filePath) => {
|
this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
|
||||||
this.installedFiles.add(filePath);
|
this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
|
||||||
},
|
|
||||||
{
|
|
||||||
isCustom: true,
|
|
||||||
moduleConfig: collectedModuleConfig,
|
|
||||||
isQuickUpdate: config._quickUpdate || false,
|
|
||||||
installer: this,
|
|
||||||
silent: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create module config (include collected config from module.yaml prompts)
|
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes
|
||||||
await this.generateModuleConfigs(bmadDir, {
|
// This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
|
||||||
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
message('Generating manifests...');
|
||||||
});
|
const manifestGen = new ManifestGenerator();
|
||||||
|
|
||||||
|
const allModulesForManifest = config._quickUpdate
|
||||||
|
? config._existingModules || allModules || []
|
||||||
|
: config._preserveModules
|
||||||
|
? [...allModules, ...config._preserveModules]
|
||||||
|
: allModules || [];
|
||||||
|
|
||||||
|
let modulesForCsvPreserve;
|
||||||
|
if (config._quickUpdate) {
|
||||||
|
modulesForCsvPreserve = config._existingModules || allModules || [];
|
||||||
} else {
|
} else {
|
||||||
// Regular module installation
|
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
|
||||||
// Special case for core module
|
|
||||||
if (moduleName === 'core') {
|
|
||||||
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
|
|
||||||
} else {
|
|
||||||
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
|
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
|
||||||
}
|
ides: config.ides || [],
|
||||||
|
preservedModules: modulesForCsvPreserve,
|
||||||
|
});
|
||||||
|
|
||||||
// Install partial modules (only dependencies)
|
addResult(
|
||||||
for (const [module, files] of Object.entries(resolution.byModule)) {
|
'Manifests',
|
||||||
if (!allModules.includes(module) && module !== 'core') {
|
'ok',
|
||||||
const totalFiles =
|
`${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`,
|
||||||
files.agents.length +
|
);
|
||||||
files.tasks.length +
|
|
||||||
files.tools.length +
|
|
||||||
files.templates.length +
|
|
||||||
files.data.length +
|
|
||||||
files.other.length;
|
|
||||||
if (totalFiles > 0) {
|
|
||||||
spinner.message(`Installing ${module} dependencies...`);
|
|
||||||
await this.installPartialModule(module, bmadDir, files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All content is now installed as modules - no separate custom content handling needed
|
// Merge help catalogs
|
||||||
|
message('Generating help catalog...');
|
||||||
|
await this.mergeModuleHelpCatalogs(bmadDir);
|
||||||
|
addResult('Help catalog', 'ok');
|
||||||
|
|
||||||
// Generate clean config.yaml files for each installed module
|
return 'Configurations generated';
|
||||||
spinner.message('Generating module configurations...');
|
},
|
||||||
await this.generateModuleConfigs(bmadDir, moduleConfigs);
|
|
||||||
addResult('Configurations', 'ok', 'generated');
|
|
||||||
|
|
||||||
// Create agent configuration 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');
|
|
||||||
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
|
|
||||||
this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
|
|
||||||
this.installedFiles.add(path.join(cfgDir, 'agent-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
|
|
||||||
// This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
|
|
||||||
spinner.message('Generating workflow and agent manifests...');
|
|
||||||
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
|
|
||||||
? config._existingModules || allModules || []
|
|
||||||
: config._preserveModules
|
|
||||||
? [...allModules, ...config._preserveModules]
|
|
||||||
: allModules || [];
|
|
||||||
|
|
||||||
// For regular installs (including when called from quick update), use what we have
|
|
||||||
let modulesForCsvPreserve;
|
|
||||||
if (config._quickUpdate) {
|
|
||||||
// Quick update - use existing modules or fall back to modules being updated
|
|
||||||
modulesForCsvPreserve = config._existingModules || allModules || [];
|
|
||||||
} else {
|
|
||||||
// Regular install - use the modules we're installing plus any preserved ones
|
|
||||||
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
|
|
||||||
ides: config.ides || [],
|
|
||||||
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Custom modules are now included in the main modules list - no separate tracking needed
|
await prompts.tasks(installTasks);
|
||||||
|
|
||||||
addResult(
|
// Resolution is now available via closure-scoped taskResolution
|
||||||
'Manifests',
|
const resolution = taskResolution;
|
||||||
'ok',
|
|
||||||
`${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Merge all module-help.csv files into bmad-help.csv
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// This must happen AFTER generateManifests because it depends on agent-manifest.csv
|
// IDE SETUP: Keep as spinner since it may prompt for user input
|
||||||
spinner.message('Generating workflow help catalog...');
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
await this.mergeModuleHelpCatalogs(bmadDir);
|
|
||||||
addResult('Help catalog', 'ok');
|
|
||||||
|
|
||||||
// Configure IDEs and copy documentation
|
|
||||||
if (!config.skipIde && config.ides && config.ides.length > 0) {
|
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();
|
await this.ideManager.ensureInitialized();
|
||||||
|
|
||||||
// Filter out any undefined/null values from the IDE list
|
|
||||||
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...');
|
||||||
|
|
||||||
for (const ide of validIdes) {
|
try {
|
||||||
if (!needsPrompting || ideConfigurations[ide]) {
|
for (const ide of validIdes) {
|
||||||
// All IDEs pre-configured, or this specific IDE has config: keep spinner running
|
if (!needsPrompting || ideConfigurations[ide]) {
|
||||||
spinner.message(`Configuring ${ide}...`);
|
ideSpinner.message(`Configuring ${ide}...`);
|
||||||
} else {
|
|
||||||
// This IDE needs prompting: stop spinner to allow user interaction
|
|
||||||
if (spinner.isSpinning) {
|
|
||||||
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)
|
|
||||||
const originalLog = console.log;
|
|
||||||
if (!config.verbose && ideHasConfig) {
|
|
||||||
console.log = () => {};
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const setupResult = await this.ideManager.setup(ide, projectDir, bmadDir, {
|
|
||||||
selectedModules: allModules || [],
|
|
||||||
preCollectedConfig: ideConfigurations[ide] || null,
|
|
||||||
verbose: config.verbose,
|
|
||||||
silent: ideHasConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save IDE configuration for future updates
|
|
||||||
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
|
|
||||||
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect result for summary
|
|
||||||
if (setupResult.success) {
|
|
||||||
addResult(ide, 'ok', setupResult.detail || '');
|
|
||||||
} else {
|
} else {
|
||||||
addResult(ide, 'error', setupResult.error || 'failed');
|
if (ideSpinner.isSpinning) {
|
||||||
|
ideSpinner.stop('Ready for IDE configuration');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
console.log = originalLog;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart spinner if we stopped it for prompting
|
// Suppress stray console output for pre-configured IDEs (no user interaction)
|
||||||
if (needsPrompting && !spinner.isSpinning) {
|
const ideHasConfig = Boolean(ideConfigurations[ide]);
|
||||||
spinner.start('Configuring IDEs...');
|
const originalLog = console.log;
|
||||||
|
if (!config.verbose && ideHasConfig) {
|
||||||
|
console.log = () => {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const setupResult = await this.ideManager.setup(ide, projectDir, bmadDir, {
|
||||||
|
selectedModules: allModules || [],
|
||||||
|
preCollectedConfig: ideConfigurations[ide] || null,
|
||||||
|
verbose: config.verbose,
|
||||||
|
silent: ideHasConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
|
||||||
|
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setupResult.success) {
|
||||||
|
addResult(ide, 'ok', setupResult.detail || '');
|
||||||
|
} else {
|
||||||
|
addResult(ide, 'error', setupResult.error || 'failed');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
console.log = originalLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsPrompting && !ideSpinner.isSpinning) {
|
||||||
|
ideSpinner.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 verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
|
const dirResults = { createdDirs: [], createdWdsFolders: [] };
|
||||||
const moduleLogger = {
|
|
||||||
log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined),
|
|
||||||
error: async (msg) => await prompts.log.error(msg),
|
|
||||||
warn: async (msg) => await prompts.log.warn(msg),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create directories for all modules
|
// Module directory creation task
|
||||||
spinner.message('Creating module directories...');
|
postIdeTasks.push({
|
||||||
const allCreatedDirs = [];
|
title: 'Running module installers',
|
||||||
const allCreatedWdsFolders = [];
|
task: async (message) => {
|
||||||
|
const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
|
||||||
|
const moduleLogger = {
|
||||||
|
log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined),
|
||||||
|
error: async (msg) => await prompts.log.error(msg),
|
||||||
|
warn: async (msg) => await prompts.log.warn(msg),
|
||||||
|
};
|
||||||
|
|
||||||
// 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, {
|
||||||
installedIDEs: config.ides || [],
|
installedIDEs: config.ides || [],
|
||||||
moduleConfig: moduleConfigs.core || {},
|
moduleConfig: moduleConfigs.core || {},
|
||||||
coreConfig: moduleConfigs.core || {},
|
coreConfig: moduleConfigs.core || {},
|
||||||
logger: moduleLogger,
|
logger: moduleLogger,
|
||||||
silent: true,
|
silent: true,
|
||||||
|
});
|
||||||
|
if (result) {
|
||||||
|
dirResults.createdDirs.push(...result.createdDirs);
|
||||||
|
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User-selected module directories
|
||||||
|
if (config.modules && config.modules.length > 0) {
|
||||||
|
for (const moduleName of config.modules) {
|
||||||
|
message(`Setting up ${moduleName}...`);
|
||||||
|
const result = await this.moduleManager.createModuleDirectories(moduleName, bmadDir, {
|
||||||
|
installedIDEs: config.ides || [],
|
||||||
|
moduleConfig: moduleConfigs[moduleName] || {},
|
||||||
|
coreConfig: moduleConfigs.core || {},
|
||||||
|
logger: moduleLogger,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
if (result) {
|
||||||
|
dirResults.createdDirs.push(...result.createdDirs);
|
||||||
|
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addResult('Module installers', 'ok');
|
||||||
|
return 'Module setup complete';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// File restoration task (only for updates)
|
||||||
|
if (
|
||||||
|
config._isUpdate &&
|
||||||
|
((config._customFiles && config._customFiles.length > 0) || (config._modifiedFiles && config._modifiedFiles.length > 0))
|
||||||
|
) {
|
||||||
|
postIdeTasks.push({
|
||||||
|
title: 'Finalizing installation',
|
||||||
|
task: async (message) => {
|
||||||
|
let customFiles = [];
|
||||||
|
let modifiedFiles = [];
|
||||||
|
|
||||||
|
if (config._customFiles && config._customFiles.length > 0) {
|
||||||
|
message(`Restoring ${config._customFiles.length} custom files...`);
|
||||||
|
|
||||||
|
for (const originalPath of config._customFiles) {
|
||||||
|
const relativePath = path.relative(bmadDir, originalPath);
|
||||||
|
const backupPath = path.join(config._tempBackupDir, relativePath);
|
||||||
|
|
||||||
|
if (await fs.pathExists(backupPath)) {
|
||||||
|
await fs.ensureDir(path.dirname(originalPath));
|
||||||
|
await fs.copy(backupPath, originalPath, { overwrite: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
|
||||||
|
await fs.remove(config._tempBackupDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
customFiles = config._customFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config._modifiedFiles && config._modifiedFiles.length > 0) {
|
||||||
|
modifiedFiles = config._modifiedFiles;
|
||||||
|
|
||||||
|
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
|
||||||
|
message(`Restoring ${modifiedFiles.length} modified files as .bak...`);
|
||||||
|
|
||||||
|
for (const modifiedFile of modifiedFiles) {
|
||||||
|
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
||||||
|
const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath);
|
||||||
|
const bakPath = modifiedFile.path + '.bak';
|
||||||
|
|
||||||
|
if (await fs.pathExists(tempBackupPath)) {
|
||||||
|
await fs.ensureDir(path.dirname(bakPath));
|
||||||
|
await fs.copy(tempBackupPath, bakPath, { overwrite: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.remove(config._tempModifiedBackupDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store for summary access
|
||||||
|
config._restoredCustomFiles = customFiles;
|
||||||
|
config._restoredModifiedFiles = modifiedFiles;
|
||||||
|
|
||||||
|
return 'Installation finalized';
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (result) {
|
|
||||||
allCreatedDirs.push(...result.createdDirs);
|
|
||||||
allCreatedWdsFolders.push(...result.createdWdsFolders);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// User-selected module directories
|
await prompts.tasks(postIdeTasks);
|
||||||
if (config.modules && config.modules.length > 0) {
|
|
||||||
for (const moduleName of config.modules) {
|
|
||||||
const result = await this.moduleManager.createModuleDirectories(moduleName, bmadDir, {
|
|
||||||
installedIDEs: config.ides || [],
|
|
||||||
moduleConfig: moduleConfigs[moduleName] || {},
|
|
||||||
coreConfig: moduleConfigs.core || {},
|
|
||||||
logger: moduleLogger,
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
allCreatedDirs.push(...result.createdDirs);
|
|
||||||
allCreatedWdsFolders.push(...result.createdWdsFolders);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch output: single log message for all created directories across all modules
|
// Render directory creation output after tasks() to avoid breaking progress display
|
||||||
if (allCreatedDirs.length > 0) {
|
if (dirResults.createdDirs.length > 0) {
|
||||||
const color = await prompts.getColor();
|
const color = await prompts.getColor();
|
||||||
const lines = allCreatedDirs.map((d) => ` ${d}`).join('\n');
|
const lines = dirResults.createdDirs.map((d) => ` ${d}`).join('\n');
|
||||||
await prompts.log.message(color.yellow(`Created directories:\n${lines}`));
|
await prompts.log.message(color.yellow(`Created directories:\n${lines}`));
|
||||||
}
|
}
|
||||||
if (allCreatedWdsFolders.length > 0) {
|
if (dirResults.createdWdsFolders.length > 0) {
|
||||||
const color = await prompts.getColor();
|
const color = await prompts.getColor();
|
||||||
const lines = allCreatedWdsFolders.map((f) => color.dim(` ✓ ${f}/`)).join('\n');
|
const lines = dirResults.createdWdsFolders.map((f) => color.dim(` ✓ ${f}/`)).join('\n');
|
||||||
await prompts.log.message(color.cyan(`Created WDS folder structure:\n${lines}`));
|
await prompts.log.message(color.cyan(`Created WDS folder structure:\n${lines}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
addResult('Module installers', 'ok');
|
// Retrieve restored file info for summary
|
||||||
|
const customFiles = config._restoredCustomFiles || [];
|
||||||
// Note: Manifest files are already created by ManifestGenerator above
|
const modifiedFiles = config._restoredModifiedFiles || [];
|
||||||
// No need to create legacy manifest.csv anymore
|
|
||||||
|
|
||||||
// If this was an update, restore custom files
|
|
||||||
let customFiles = [];
|
|
||||||
let modifiedFiles = [];
|
|
||||||
if (config._isUpdate) {
|
|
||||||
if (config._customFiles && config._customFiles.length > 0) {
|
|
||||||
spinner.message(`Restoring ${config._customFiles.length} custom files...`);
|
|
||||||
|
|
||||||
for (const originalPath of config._customFiles) {
|
|
||||||
const relativePath = path.relative(bmadDir, originalPath);
|
|
||||||
const backupPath = path.join(config._tempBackupDir, relativePath);
|
|
||||||
|
|
||||||
if (await fs.pathExists(backupPath)) {
|
|
||||||
await fs.ensureDir(path.dirname(originalPath));
|
|
||||||
await fs.copy(backupPath, originalPath, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up temp backup
|
|
||||||
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
|
|
||||||
await fs.remove(config._tempBackupDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
customFiles = config._customFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config._modifiedFiles && config._modifiedFiles.length > 0) {
|
|
||||||
modifiedFiles = config._modifiedFiles;
|
|
||||||
|
|
||||||
// Restore modified files as .bak files
|
|
||||||
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
|
|
||||||
spinner.message(`Restoring ${modifiedFiles.length} modified files as .bak...`);
|
|
||||||
|
|
||||||
for (const modifiedFile of modifiedFiles) {
|
|
||||||
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
|
||||||
const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath);
|
|
||||||
const bakPath = modifiedFile.path + '.bak';
|
|
||||||
|
|
||||||
if (await fs.pathExists(tempBackupPath)) {
|
|
||||||
await fs.ensureDir(path.dirname(bakPath));
|
|
||||||
await fs.copy(tempBackupPath, bakPath, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up temp backup
|
|
||||||
await fs.remove(config._tempModifiedBackupDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blank line for spacing before final status
|
|
||||||
await prompts.log.message('');
|
|
||||||
|
|
||||||
// Stop the single installation spinner
|
|
||||||
spinner.stop('Installation complete');
|
|
||||||
|
|
||||||
// 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) {
|
||||||
spinner.error('Installation failed');
|
try {
|
||||||
|
if (spinner.isSpinning) {
|
||||||
|
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!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue