refactor(installer): remove spinners and extract execution methods

Remove all spinner management from install(), quickUpdate(), update(),
_prepareUpdateState(), and _backupUserFiles(). Extract four named
methods from the install() execution tail: _cacheCustomModules(),
_buildModuleLists(), _setupIdes(), _restoreUserFiles().
This commit is contained in:
Alex Verkhovsky 2026-03-21 20:54:58 -06:00
parent 1a519b9748
commit 1befede51d
1 changed files with 190 additions and 271 deletions

View File

@ -62,17 +62,10 @@ class Installer {
// Tool selection will be collected after we determine if it's a reinstall/update/new install // Tool selection will be collected after we determine if it's a reinstall/update/new install
const spinner = await prompts.spinner();
spinner.start('Preparing installation...');
try { try {
// Check existing installation
spinner.message('Checking for existing installation...');
const existingInstall = await this.detector.detect(paths.bmadDir); const existingInstall = await this.detector.detect(paths.bmadDir);
if (existingInstall.installed && !config.force && !config.isQuickUpdate()) { if (existingInstall.installed && !config.force && !config.isQuickUpdate()) {
spinner.stop('Existing installation detected');
// Check if user already decided what to do (from early menu in ui.js) // Check if user already decided what to do (from early menu in ui.js)
let action = null; let action = null;
if (config.actionType === 'update') { if (config.actionType === 'update') {
@ -107,12 +100,7 @@ class Installer {
if (!config.modules) config.modules = []; if (!config.modules) config.modules = [];
config.modules.push(moduleId); config.modules.push(moduleId);
} }
spinner.start('Preparing update...');
} else { } else {
if (spinner.isSpinning) {
spinner.stop('Module changes reviewed');
}
await prompts.log.warn('Modules to be removed:'); await prompts.log.warn('Modules to be removed:');
for (const moduleId of modulesToRemove) { for (const moduleId of modulesToRemove) {
const moduleInfo = existingInstall.modules.find((m) => m.id === moduleId); const moduleInfo = existingInstall.modules.find((m) => m.id === moduleId);
@ -148,22 +136,17 @@ class Installer {
config.modules.push(moduleId); config.modules.push(moduleId);
} }
} }
spinner.start('Preparing update...');
} }
} }
await this._prepareUpdateState(paths, config, customConfig, existingInstall, spinner); await this._prepareUpdateState(paths, config, customConfig, existingInstall);
} }
} else if (existingInstall.installed && config.isQuickUpdate()) { } else if (existingInstall.installed && config.isQuickUpdate()) {
// Quick update mode - automatically treat as update without prompting await this._prepareUpdateState(paths, config, customConfig, existingInstall);
spinner.message('Preparing quick update...');
await this._prepareUpdateState(paths, config, customConfig, existingInstall, spinner);
} }
// Now collect tool configurations after we know if it's a reinstall // Now collect tool configurations after we know if it's a reinstall
// Skip for quick update since we already have the IDE list // Skip for quick update since we already have the IDE list
spinner.stop('Pre-checks complete');
let toolSelection; let toolSelection;
if (config.isQuickUpdate()) { if (config.isQuickUpdate()) {
// Quick update already has IDEs configured, use saved configurations // Quick update already has IDEs configured, use saved configurations
@ -242,10 +225,6 @@ class Installer {
} }
} }
} else { } else {
if (spinner.isSpinning) {
spinner.stop('IDE changes reviewed');
}
await prompts.log.warn('IDEs to be removed:'); await prompts.log.warn('IDEs to be removed:');
for (const ide of idesToRemove) { for (const ide of idesToRemove) {
await prompts.log.error(` - ${ide}`); await prompts.log.error(` - ${ide}`);
@ -283,8 +262,6 @@ class Installer {
} }
} }
} }
spinner.start('Preparing installation...');
} }
} }
} }
@ -293,72 +270,10 @@ class Installer {
const results = []; const results = [];
const addResult = (step, status, detail = '') => results.push({ step, status, detail }); const addResult = (step, status, detail = '') => results.push({ step, status, detail });
if (spinner.isSpinning) { await this._cacheCustomModules(paths, addResult);
spinner.message('Preparing installation...');
} else {
spinner.start('Preparing installation...');
}
// Cache custom modules if any
if (this.customModules.paths && this.customModules.paths.size > 0) {
spinner.message('Caching custom modules...');
const { CustomModuleCache } = require('./custom-module-cache');
const customCache = new CustomModuleCache(paths.bmadDir);
for (const [moduleId, sourcePath] of this.customModules.paths) {
const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
sourcePath: sourcePath, // Store original path for updates
});
// Update cached path to use the local cache location
this.customModules.paths.set(moduleId, cachedInfo.cachePath);
}
addResult('Custom modules cached', 'ok');
}
// Custom content is already handled in UI before module selection
const finalCustomContent = customConfig.customContent; const finalCustomContent = customConfig.customContent;
const { officialModules, allModules } = await this._buildModuleLists(config, customConfig, paths);
// Build custom module ID set first (needed to filter official list)
const customModuleIds = new Set();
for (const id of this.customModules.paths.keys()) {
customModuleIds.add(id);
}
if (customConfig._customModuleSources) {
for (const [moduleId, customInfo] of customConfig._customModuleSources) {
if (!customModuleIds.has(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
customModuleIds.add(moduleId);
}
}
}
if (finalCustomContent && finalCustomContent.cachedModules) {
for (const cachedModule of finalCustomContent.cachedModules) {
customModuleIds.add(cachedModule.id);
}
}
if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
const customHandler = new CustomHandler();
for (const customFile of finalCustomContent.selectedFiles) {
const customInfo = await customHandler.getCustomInfo(customFile, paths.projectRoot);
if (customInfo && customInfo.id) {
customModuleIds.add(customInfo.id);
}
}
}
// Official modules: from config.modules, excluding core (handled separately) and custom modules
const officialModules = (config.modules || []).filter((m) => !customModuleIds.has(m));
// Combined list for manifest generation and IDE setup
const allModules = [...officialModules];
for (const id of customModuleIds) {
if (!allModules.includes(id)) {
allModules.push(id);
}
}
// Stop spinner before tasks() takes over progress display
spinner.stop('Preparation complete');
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
// FIRST TASKS BLOCK: Core installation through manifests (non-interactive) // FIRST TASKS BLOCK: Core installation through manifests (non-interactive)
@ -496,143 +411,13 @@ class Installer {
// Now run configuration generation // Now run configuration generation
await prompts.tasks([configTask]); await prompts.tasks([configTask]);
// ───────────────────────────────────────────────────────────────────────── await this._setupIdes(config, ideConfigurations, allModules, paths, addResult);
// 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');
if (validIdes.length === 0) { await this._restoreUserFiles(paths, customConfig);
addResult('IDE configuration', 'warn', 'no valid IDEs selected');
} else {
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
const ideSpinner = await prompts.spinner();
ideSpinner.start('Configuring tools...');
try {
for (const ide of validIdes) {
if (!needsPrompting || ideConfigurations[ide]) {
ideSpinner.message(`Configuring ${ide}...`);
} else {
if (ideSpinner.isSpinning) {
ideSpinner.stop('Ready for IDE configuration');
}
}
// Suppress stray console output for pre-configured IDEs (no user interaction)
const ideHasConfig = Boolean(ideConfigurations[ide]);
const originalLog = console.log;
if (!config.verbose && ideHasConfig) {
console.log = () => {};
}
try {
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
selectedModules: allModules || [],
preCollectedConfig: ideConfigurations[ide] || null,
verbose: config.verbose,
silent: ideHasConfig,
});
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
await this.ideConfigManager.saveIdeConfig(paths.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 tools...');
}
}
} finally {
if (ideSpinner.isSpinning) {
ideSpinner.stop('Tool configuration complete');
}
}
}
}
// ─────────────────────────────────────────────────────────────────────────
// SECOND TASKS BLOCK: Post-IDE operations (non-interactive)
// ─────────────────────────────────────────────────────────────────────────
const postIdeTasks = [];
// File restoration task (only for updates)
if (
customConfig._isUpdate &&
((customConfig._customFiles && customConfig._customFiles.length > 0) ||
(customConfig._modifiedFiles && customConfig._modifiedFiles.length > 0))
) {
postIdeTasks.push({
title: 'Finalizing installation',
task: async (message) => {
let customFiles = [];
let modifiedFiles = [];
if (customConfig._customFiles && customConfig._customFiles.length > 0) {
message(`Restoring ${customConfig._customFiles.length} custom files...`);
for (const originalPath of customConfig._customFiles) {
const relativePath = path.relative(paths.bmadDir, originalPath);
const backupPath = path.join(customConfig._tempBackupDir, relativePath);
if (await fs.pathExists(backupPath)) {
await fs.ensureDir(path.dirname(originalPath));
await fs.copy(backupPath, originalPath, { overwrite: true });
}
}
if (customConfig._tempBackupDir && (await fs.pathExists(customConfig._tempBackupDir))) {
await fs.remove(customConfig._tempBackupDir);
}
customFiles = customConfig._customFiles;
}
if (customConfig._modifiedFiles && customConfig._modifiedFiles.length > 0) {
modifiedFiles = customConfig._modifiedFiles;
if (customConfig._tempModifiedBackupDir && (await fs.pathExists(customConfig._tempModifiedBackupDir))) {
message(`Restoring ${modifiedFiles.length} modified files as .bak...`);
for (const modifiedFile of modifiedFiles) {
const relativePath = path.relative(paths.bmadDir, modifiedFile.path);
const tempBackupPath = path.join(customConfig._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(customConfig._tempModifiedBackupDir);
}
}
// Store for summary access
customConfig._restoredCustomFiles = customFiles;
customConfig._restoredModifiedFiles = modifiedFiles;
return 'Installation finalized';
},
});
}
await prompts.tasks(postIdeTasks);
// Retrieve restored file info for summary
const customFiles = customConfig._restoredCustomFiles || [];
const modifiedFiles = customConfig._restoredModifiedFiles || [];
// Render consolidated summary // Render consolidated summary
const customFiles = customConfig._restoredCustomFiles || [];
const modifiedFiles = customConfig._restoredModifiedFiles || [];
await this.renderInstallSummary(results, { await this.renderInstallSummary(results, {
bmadDir: paths.bmadDir, bmadDir: paths.bmadDir,
modules: config.modules, modules: config.modules,
@ -649,15 +434,7 @@ class Installer {
projectDir: paths.projectRoot, projectDir: paths.projectRoot,
}; };
} catch (error) { } catch (error) {
try { await prompts.log.error('Installation failed');
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
}
// Clean up any temp backup directories that were created before the failure // Clean up any temp backup directories that were created before the failure
try { try {
@ -675,6 +452,184 @@ class Installer {
} }
} }
/**
* Cache custom modules into the local cache directory.
* Updates this.customModules.paths in place with cached locations.
*/
async _cacheCustomModules(paths, addResult) {
if (!this.customModules.paths || this.customModules.paths.size === 0) return;
const { CustomModuleCache } = require('./custom-module-cache');
const customCache = new CustomModuleCache(paths.bmadDir);
for (const [moduleId, sourcePath] of this.customModules.paths) {
const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
sourcePath: sourcePath,
});
this.customModules.paths.set(moduleId, cachedInfo.cachePath);
}
addResult('Custom modules cached', 'ok');
}
/**
* Build the official and combined module lists from config and custom sources.
* @returns {{ officialModules: string[], allModules: string[] }}
*/
async _buildModuleLists(config, customConfig, paths) {
const finalCustomContent = customConfig.customContent;
const customModuleIds = new Set();
for (const id of this.customModules.paths.keys()) {
customModuleIds.add(id);
}
if (customConfig._customModuleSources) {
for (const [moduleId, customInfo] of customConfig._customModuleSources) {
if (!customModuleIds.has(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
customModuleIds.add(moduleId);
}
}
}
if (finalCustomContent && finalCustomContent.cachedModules) {
for (const cachedModule of finalCustomContent.cachedModules) {
customModuleIds.add(cachedModule.id);
}
}
if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
const customHandler = new CustomHandler();
for (const customFile of finalCustomContent.selectedFiles) {
const customInfo = await customHandler.getCustomInfo(customFile, paths.projectRoot);
if (customInfo && customInfo.id) {
customModuleIds.add(customInfo.id);
}
}
}
const officialModules = (config.modules || []).filter((m) => !customModuleIds.has(m));
const allModules = [...officialModules];
for (const id of customModuleIds) {
if (!allModules.includes(id)) {
allModules.push(id);
}
}
return { officialModules, allModules };
}
/**
* Set up IDE integrations for each selected IDE.
*/
async _setupIdes(config, ideConfigurations, allModules, paths, addResult) {
if (config.skipIde || !config.ides || config.ides.length === 0) return;
await this.ideManager.ensureInitialized();
const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
if (validIdes.length === 0) {
addResult('IDE configuration', 'warn', 'no valid IDEs selected');
return;
}
for (const ide of validIdes) {
const ideHasConfig = Boolean(ideConfigurations[ide]);
const originalLog = console.log;
if (!config.verbose && ideHasConfig) {
console.log = () => {};
}
try {
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
selectedModules: allModules || [],
preCollectedConfig: ideConfigurations[ide] || null,
verbose: config.verbose,
silent: ideHasConfig,
});
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
await this.ideConfigManager.saveIdeConfig(paths.bmadDir, ide, ideConfigurations[ide]);
}
if (setupResult.success) {
addResult(ide, 'ok', setupResult.detail || '');
} else {
addResult(ide, 'error', setupResult.error || 'failed');
}
} finally {
console.log = originalLog;
}
}
}
/**
* Restore custom and modified files that were backed up before the update.
* No-op for fresh installs.
*/
async _restoreUserFiles(paths, customConfig) {
if (
!customConfig._isUpdate ||
((!customConfig._customFiles || customConfig._customFiles.length === 0) &&
(!customConfig._modifiedFiles || customConfig._modifiedFiles.length === 0))
) {
return;
}
await prompts.tasks([
{
title: 'Finalizing installation',
task: async (message) => {
let customFiles = [];
let modifiedFiles = [];
if (customConfig._customFiles && customConfig._customFiles.length > 0) {
message(`Restoring ${customConfig._customFiles.length} custom files...`);
for (const originalPath of customConfig._customFiles) {
const relativePath = path.relative(paths.bmadDir, originalPath);
const backupPath = path.join(customConfig._tempBackupDir, relativePath);
if (await fs.pathExists(backupPath)) {
await fs.ensureDir(path.dirname(originalPath));
await fs.copy(backupPath, originalPath, { overwrite: true });
}
}
if (customConfig._tempBackupDir && (await fs.pathExists(customConfig._tempBackupDir))) {
await fs.remove(customConfig._tempBackupDir);
}
customFiles = customConfig._customFiles;
}
if (customConfig._modifiedFiles && customConfig._modifiedFiles.length > 0) {
modifiedFiles = customConfig._modifiedFiles;
if (customConfig._tempModifiedBackupDir && (await fs.pathExists(customConfig._tempModifiedBackupDir))) {
message(`Restoring ${modifiedFiles.length} modified files as .bak...`);
for (const modifiedFile of modifiedFiles) {
const relativePath = path.relative(paths.bmadDir, modifiedFile.path);
const tempBackupPath = path.join(customConfig._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(customConfig._tempModifiedBackupDir);
}
}
customConfig._restoredCustomFiles = customFiles;
customConfig._restoredModifiedFiles = modifiedFiles;
return 'Installation finalized';
},
},
]);
}
_buildConfig(originalConfig) { _buildConfig(originalConfig) {
const modules = [...(originalConfig.modules || [])]; const modules = [...(originalConfig.modules || [])];
if (originalConfig.installCore && !modules.includes('core')) { if (originalConfig.installCore && !modules.includes('core')) {
@ -774,9 +729,8 @@ class Installer {
* @param {Object} config - Clean config (may have coreConfig updated) * @param {Object} config - Clean config (may have coreConfig updated)
* @param {Object} customConfig - Full config bag (mutated with update state) * @param {Object} customConfig - Full config bag (mutated with update state)
* @param {Object} existingInstall - Detection result from detector.detect() * @param {Object} existingInstall - Detection result from detector.detect()
* @param {Object} spinner - Spinner instance for progress display
*/ */
async _prepareUpdateState(paths, config, customConfig, existingInstall, spinner) { async _prepareUpdateState(paths, config, customConfig, existingInstall) {
customConfig._isUpdate = true; customConfig._isUpdate = true;
customConfig._existingInstall = existingInstall; customConfig._existingInstall = existingInstall;
@ -806,7 +760,7 @@ class Installer {
await this._scanCachedCustomModules(paths); await this._scanCachedCustomModules(paths);
const backupDirs = await this._backupUserFiles(paths, customFiles, modifiedFiles, spinner); const backupDirs = await this._backupUserFiles(paths, customFiles, modifiedFiles);
customConfig._tempBackupDir = backupDirs.tempBackupDir; customConfig._tempBackupDir = backupDirs.tempBackupDir;
customConfig._tempModifiedBackupDir = backupDirs.tempModifiedBackupDir; customConfig._tempModifiedBackupDir = backupDirs.tempModifiedBackupDir;
} }
@ -817,10 +771,9 @@ class Installer {
* @param {Object} paths - InstallPaths instance * @param {Object} paths - InstallPaths instance
* @param {string[]} customFiles - Absolute paths of custom (user-added) files * @param {string[]} customFiles - Absolute paths of custom (user-added) files
* @param {Object[]} modifiedFiles - Array of { path, relativePath } for modified files * @param {Object[]} modifiedFiles - Array of { path, relativePath } for modified files
* @param {Object} spinner - Spinner instance for progress display
* @returns {Object} { tempBackupDir, tempModifiedBackupDir } undefined if no files * @returns {Object} { tempBackupDir, tempModifiedBackupDir } undefined if no files
*/ */
async _backupUserFiles(paths, customFiles, modifiedFiles, spinner) { async _backupUserFiles(paths, customFiles, modifiedFiles) {
let tempBackupDir; let tempBackupDir;
let tempModifiedBackupDir; let tempModifiedBackupDir;
@ -828,28 +781,24 @@ class Installer {
tempBackupDir = path.join(paths.projectRoot, '_bmad-custom-backup-temp'); tempBackupDir = path.join(paths.projectRoot, '_bmad-custom-backup-temp');
await fs.ensureDir(tempBackupDir); await fs.ensureDir(tempBackupDir);
spinner.start(`Backing up ${customFiles.length} custom files...`);
for (const customFile of customFiles) { for (const customFile of customFiles) {
const relativePath = path.relative(paths.bmadDir, customFile); const relativePath = path.relative(paths.bmadDir, customFile);
const backupPath = path.join(tempBackupDir, relativePath); const backupPath = path.join(tempBackupDir, relativePath);
await fs.ensureDir(path.dirname(backupPath)); await fs.ensureDir(path.dirname(backupPath));
await fs.copy(customFile, backupPath); await fs.copy(customFile, backupPath);
} }
spinner.stop(`Backed up ${customFiles.length} custom files`);
} }
if (modifiedFiles.length > 0) { if (modifiedFiles.length > 0) {
tempModifiedBackupDir = path.join(paths.projectRoot, '_bmad-modified-backup-temp'); tempModifiedBackupDir = path.join(paths.projectRoot, '_bmad-modified-backup-temp');
await fs.ensureDir(tempModifiedBackupDir); await fs.ensureDir(tempModifiedBackupDir);
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
for (const modifiedFile of modifiedFiles) { for (const modifiedFile of modifiedFiles) {
const relativePath = path.relative(paths.bmadDir, modifiedFile.path); const relativePath = path.relative(paths.bmadDir, modifiedFile.path);
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath); const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
await fs.ensureDir(path.dirname(tempBackupPath)); await fs.ensureDir(path.dirname(tempBackupPath));
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true }); await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
} }
spinner.stop(`Backed up ${modifiedFiles.length} modified files`);
} }
return { tempBackupDir, tempModifiedBackupDir }; return { tempBackupDir, tempModifiedBackupDir };
@ -1640,21 +1589,15 @@ class Installer {
* @returns {Object} Update result * @returns {Object} Update result
*/ */
async quickUpdate(config) { async quickUpdate(config) {
const spinner = await prompts.spinner();
spinner.start('Starting quick update...');
try { try {
const projectDir = path.resolve(config.directory); const projectDir = path.resolve(config.directory);
const { bmadDir } = await this.findBmadDir(projectDir); const { bmadDir } = await this.findBmadDir(projectDir);
// Check if bmad directory exists // Check if bmad directory exists
if (!(await fs.pathExists(bmadDir))) { if (!(await fs.pathExists(bmadDir))) {
spinner.stop('No BMAD installation found');
throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`); throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
} }
spinner.message('Detecting installed modules and configuration...');
// Detect existing installation // Detect existing installation
const existingInstall = await this.detector.detect(bmadDir); const existingInstall = await this.detector.detect(bmadDir);
const installedModules = existingInstall.modules.map((m) => m.id); const installedModules = existingInstall.modules.map((m) => m.id);
@ -1795,8 +1738,6 @@ class Installer {
} }
} }
spinner.stop(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`);
if (skippedModules.length > 0) { if (skippedModules.length > 0) {
await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`); await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`);
} }
@ -1850,12 +1791,6 @@ class Installer {
// Call the standard install method // Call the standard install method
const result = await this.install(installConfig); const result = await this.install(installConfig);
// Only succeed the spinner if it's still spinning
// (install method might have stopped it if folder name changed)
if (spinner.isSpinning) {
spinner.stop('Quick update complete!');
}
return { return {
success: true, success: true,
moduleCount: modulesToUpdate.length, moduleCount: modulesToUpdate.length,
@ -1865,7 +1800,6 @@ class Installer {
ides: configuredIdes, ides: configuredIdes,
}; };
} catch (error) { } catch (error) {
spinner.error('Quick update failed');
throw error; throw error;
} }
} }
@ -1874,21 +1808,15 @@ class Installer {
* Update existing installation * Update existing installation
*/ */
async update(config) { async update(config) {
const spinner = await prompts.spinner();
spinner.start('Checking installation...');
try { try {
const projectDir = path.resolve(config.directory); const projectDir = path.resolve(config.directory);
const { bmadDir } = await this.findBmadDir(projectDir); const { bmadDir } = await this.findBmadDir(projectDir);
const existingInstall = await this.detector.detect(bmadDir); const existingInstall = await this.detector.detect(bmadDir);
if (!existingInstall.installed) { if (!existingInstall.installed) {
spinner.stop('No BMAD installation found');
throw new Error(`No BMAD installation found at ${bmadDir}`); throw new Error(`No BMAD installation found at ${bmadDir}`);
} }
spinner.message('Analyzing update requirements...');
// Compare versions and determine what needs updating // Compare versions and determine what needs updating
const currentVersion = existingInstall.version; const currentVersion = existingInstall.version;
const newVersion = require(path.join(getProjectRoot(), 'package.json')).version; const newVersion = require(path.join(getProjectRoot(), 'package.json')).version;
@ -1941,7 +1869,6 @@ class Installer {
} }
if (customModuleSources.size > 0) { if (customModuleSources.size > 0) {
spinner.stop('Update analysis complete');
await prompts.log.warn('Checking custom module sources before update...'); await prompts.log.warn('Checking custom module sources before update...');
const projectRoot = getProjectRoot(); const projectRoot = getProjectRoot();
@ -1953,12 +1880,9 @@ class Installer {
existingInstall.modules.map((m) => m.id), existingInstall.modules.map((m) => m.id),
config.skipPrompts || false, config.skipPrompts || false,
); );
spinner.start('Preparing update...');
} }
if (config.dryRun) { if (config.dryRun) {
spinner.stop('Dry run analysis complete');
let dryRunContent = `Current version: ${currentVersion}\n`; let dryRunContent = `Current version: ${currentVersion}\n`;
dryRunContent += `New version: ${newVersion}\n`; dryRunContent += `New version: ${newVersion}\n`;
dryRunContent += `Core: ${existingInstall.hasCore ? 'Will be updated' : 'Not installed'}`; dryRunContent += `Core: ${existingInstall.hasCore ? 'Will be updated' : 'Not installed'}`;
@ -1975,26 +1899,21 @@ class Installer {
// Perform actual update // Perform actual update
if (existingInstall.hasCore) { if (existingInstall.hasCore) {
spinner.message('Updating core...');
await this.updateCore(bmadDir, config.force); await this.updateCore(bmadDir, config.force);
} }
for (const module of existingInstall.modules) { for (const module of existingInstall.modules) {
spinner.message(`Updating module: ${module.id}...`);
await this.officialModules.update(module.id, bmadDir, config.force, { installer: this }); await this.officialModules.update(module.id, bmadDir, config.force, { installer: this });
} }
// Update manifest // Update manifest
spinner.message('Updating manifest...');
await this.manifest.update(bmadDir, { await this.manifest.update(bmadDir, {
version: newVersion, version: newVersion,
updateDate: new Date().toISOString(), updateDate: new Date().toISOString(),
}); });
spinner.stop('Update complete');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
spinner.error('Update failed');
throw error; throw error;
} }
} }