diff --git a/tools/cli/bmad-cli.js b/tools/cli/bmad-cli.js index 5b26362c2..bcd599293 100755 --- a/tools/cli/bmad-cli.js +++ b/tools/cli/bmad-cli.js @@ -7,7 +7,8 @@ const prompts = require('./lib/prompts'); // The installer flow uses many sequential @clack/prompts, each adding keypress // listeners to stdin. Raise the limit to avoid spurious EventEmitter warnings. if (process.stdin?.setMaxListeners) { - process.stdin.setMaxListeners(25); + const currentLimit = process.stdin.getMaxListeners(); + process.stdin.setMaxListeners(Math.max(currentLimit, 50)); } // Check for updates - do this asynchronously so it doesn't block startup diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index cfb7e1475..835e7fc99 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -164,8 +164,6 @@ class Installer { const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide)); if (newlySelectedIdes.length > 0) { - // @clack handles spacing - // Collect configuration for IDEs that support it for (const ide of newlySelectedIdes) { try { @@ -185,8 +183,10 @@ class Installer { projectDir, bmadDir, }); + } else { + // Config-driven IDEs don't need configuration - mark as ready + ideConfigurations[ide] = { _noConfigNeeded: true }; } - // Most config-driven IDEs don't need configuration - silently skip } catch (error) { // IDE doesn't support configuration or has an error await prompts.log.warn(`Warning: Could not load configuration for ${ide}: ${error.message}`); @@ -697,10 +697,14 @@ class Installer { config.skipIde = toolSelection.skipIde; const ideConfigurations = toolSelection.configurations; + // Results collector for consolidated summary + const results = []; + const addResult = (step, status, detail = '') => results.push({ step, status, detail }); + if (spinner.isSpinning) { - spinner.message('Continuing installation...'); + spinner.message('Installing...'); } else { - spinner.start('Continuing installation...'); + spinner.start('Installing...'); } // Create bmad directory structure @@ -724,16 +728,16 @@ class Installer { // Update module manager with the cached paths this.moduleManager.setCustomModulePaths(customModulePaths); - spinner.stop('Custom modules cached'); + addResult('Custom modules cached', 'ok'); } const projectRoot = getProjectRoot(); // Step 1: Install core module first (if requested) if (config.installCore) { - spinner.start('Installing BMAD core...'); + spinner.message('Installing BMAD core...'); await this.installCoreWithDependencies(bmadDir, { core: {} }); - spinner.stop('Core installed'); + addResult('Core', 'ok', 'installed'); // Generate core config file await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} }); @@ -808,7 +812,7 @@ class Installer { moduleManager: tempModuleManager, }); - spinner.stop('Dependencies resolved'); + spinner.message('Resolving dependencies...'); // Install modules with their dependencies if (allModules && allModules.length > 0) { @@ -823,7 +827,7 @@ class Installer { // Show appropriate message based on whether this is a quick update const isQuickUpdate = config._quickUpdate || false; - spinner.start(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`); + spinner.message(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`); // Check if this is a custom module let isCustomModule = false; @@ -897,6 +901,7 @@ class Installer { moduleConfig: collectedModuleConfig, isQuickUpdate: config._quickUpdate || false, installer: this, + silent: true, }, ); @@ -914,7 +919,7 @@ class Installer { } } - spinner.stop(`Module ${isQuickUpdate ? 'updated' : 'installed'}: ${moduleName}`); + addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed'); } // Install partial modules (only dependencies) @@ -928,9 +933,8 @@ class Installer { files.data.length + files.other.length; if (totalFiles > 0) { - spinner.start(`Installing ${module} dependencies...`); + spinner.message(`Installing ${module} dependencies...`); await this.installPartialModule(module, bmadDir, files); - spinner.stop(`${module} dependencies installed`); } } } @@ -939,9 +943,9 @@ class Installer { // All content is now installed as modules - no separate custom content handling needed // Generate clean config.yaml files for each installed module - spinner.start('Generating module configurations...'); + spinner.message('Generating module configurations...'); await this.generateModuleConfigs(bmadDir, moduleConfigs); - spinner.stop('Module configurations generated'); + addResult('Configurations', 'ok', 'generated'); // Create agent configuration files // Note: Legacy createAgentConfigs removed - using YAML customize system instead @@ -956,7 +960,7 @@ class Installer { // 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.start('Generating workflow and agent manifests...'); + spinner.message('Generating workflow and agent manifests...'); const manifestGen = new ManifestGenerator(); // For quick update, we need ALL installed modules in the manifest @@ -984,15 +988,17 @@ class Installer { // Custom modules are now included in the main modules list - no separate tracking needed - spinner.stop( - `Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`, + addResult( + 'Manifests', + '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 - spinner.start('Generating workflow help catalog...'); + spinner.message('Generating workflow help catalog...'); await this.mergeModuleHelpCatalogs(bmadDir); - spinner.stop('Workflow help catalog generated'); + addResult('Help catalog', 'ok'); // Configure IDEs and copy documentation if (!config.skipIde && config.ides && config.ides.length > 0) { @@ -1003,15 +1009,11 @@ class Installer { const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string'); if (validIdes.length === 0) { - await prompts.log.warn('No valid IDEs selected. Skipping IDE configuration.'); + addResult('IDE configuration', 'warn', 'no valid IDEs selected'); } else { // Check if any IDE might need prompting (no pre-collected config) const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]); - if (!needsPrompting) { - spinner.start('Configuring IDEs...'); - } - // Temporarily suppress console output if not verbose const originalLog = console.log; if (!config.verbose) { @@ -1019,22 +1021,23 @@ class Installer { } for (const ide of validIdes) { - // Only show spinner if we have pre-collected config (no prompts expected) - if (ideConfigurations[ide] && !needsPrompting) { + if (!needsPrompting || ideConfigurations[ide]) { + // All IDEs pre-configured, or this specific IDE has config: keep spinner running spinner.message(`Configuring ${ide}...`); - } else if (!ideConfigurations[ide]) { - // Stop spinner before prompting + } else { + // This IDE needs prompting: stop spinner to allow user interaction if (spinner.isSpinning) { spinner.stop('Ready for IDE configuration'); } - await prompts.log.info(`Configuring ${ide}...`); } - // Pass pre-collected configuration to avoid re-prompting - await this.ideManager.setup(ide, projectDir, bmadDir, { + // Silent when this IDE has pre-collected config (no prompts for THIS IDE) + const ideHasConfig = Boolean(ideConfigurations[ide]); + 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 @@ -1042,25 +1045,26 @@ class Installer { await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); } - // Restart spinner if we stopped it - if (!ideConfigurations[ide] && !spinner.isSpinning) { + // Collect result for summary + if (setupResult.success) { + addResult(ide, 'ok', setupResult.detail || ''); + } else { + addResult(ide, 'error', setupResult.error || 'failed'); + } + + // Restart spinner if we stopped it for prompting + if (needsPrompting && !spinner.isSpinning) { spinner.start('Configuring IDEs...'); } } // Restore console.log console.log = originalLog; - - if (spinner.isSpinning) { - spinner.stop(`Configured: ${validIdes.join(', ')}`); - } else { - await prompts.log.success(`Configured: ${validIdes.join(', ')}`); - } } } // Run module-specific installers after IDE setup - spinner.start('Running module-specific installers...'); + spinner.message('Running module-specific installers...'); // Create a conditional logger based on verbose mode const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose; @@ -1079,6 +1083,7 @@ class Installer { moduleConfig: moduleConfigs.core || {}, coreConfig: moduleConfigs.core || {}, logger: moduleLogger, + silent: true, }); } @@ -1093,11 +1098,12 @@ class Installer { moduleConfig: moduleConfigs[moduleName] || {}, coreConfig: moduleConfigs.core || {}, logger: moduleLogger, + silent: true, }); } } - spinner.stop('Module-specific installers completed'); + addResult('Module installers', 'ok'); // Note: Manifest files are already created by ManifestGenerator above // No need to create legacy manifest.csv anymore @@ -1107,7 +1113,7 @@ class Installer { let modifiedFiles = []; if (config._isUpdate) { if (config._customFiles && config._customFiles.length > 0) { - spinner.start(`Restoring ${config._customFiles.length} custom files...`); + spinner.message(`Restoring ${config._customFiles.length} custom files...`); for (const originalPath of config._customFiles) { const relativePath = path.relative(bmadDir, originalPath); @@ -1124,7 +1130,6 @@ class Installer { await fs.remove(config._tempBackupDir); } - spinner.stop(`Restored ${config._customFiles.length} custom files`); customFiles = config._customFiles; } @@ -1133,7 +1138,7 @@ class Installer { // Restore modified files as .bak files if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) { - spinner.start(`Restoring ${modifiedFiles.length} modified files as .bak...`); + spinner.message(`Restoring ${modifiedFiles.length} modified files as .bak...`); for (const modifiedFile of modifiedFiles) { const relativePath = path.relative(bmadDir, modifiedFile.path); @@ -1148,37 +1153,20 @@ class Installer { // Clean up temp backup await fs.remove(config._tempModifiedBackupDir); - - spinner.stop(`Restored ${modifiedFiles.length} modified files as .bak`); } } } - if (spinner.isSpinning) { - spinner.stop('Installation finalized'); - } + // Stop the single installation spinner + spinner.stop('Installation complete'); - // Report custom and modified files if any were found - if (customFiles.length > 0) { - await prompts.log.info(`Custom files preserved: ${customFiles.length}`); - } - - if (modifiedFiles.length > 0) { - await prompts.log.warn(`User modified files detected: ${modifiedFiles.length}`); - await prompts.log.message( - 'These user modified files have been updated with the new version, search the project for .bak files that had your customizations.', - ); - await prompts.log.message('Remove these .bak files if no longer needed'); - } - - // Display completion message - const { UI } = require('../../../lib/ui'); - const ui = new UI(); - await ui.showInstallSummary({ - path: bmadDir, + // Render consolidated summary + await this.renderInstallSummary(results, { + bmadDir, modules: config.modules, ides: config.ides, customFiles: customFiles.length > 0 ? customFiles : undefined, + modifiedFiles: modifiedFiles.length > 0 ? modifiedFiles : undefined, }); return { @@ -1194,6 +1182,52 @@ class Installer { } } + /** + * Render a consolidated install summary using prompts.note() + * @param {Array} results - Array of {step, status: 'ok'|'error'|'warn', detail} + * @param {Object} context - {bmadDir, modules, ides, customFiles, modifiedFiles} + */ + async renderInstallSummary(results, context = {}) { + const color = await prompts.getColor(); + + // Build step lines with status indicators + const lines = []; + for (const r of results) { + let icon; + if (r.status === 'ok') { + icon = color.green('\u2713'); + } else if (r.status === 'warn') { + icon = color.yellow('!'); + } else { + icon = color.red('\u2717'); + } + const detail = r.detail ? color.dim(` (${r.detail})`) : ''; + lines.push(` ${icon} ${r.step}${detail}`); + } + + // Add context info + lines.push(''); + if (context.bmadDir) { + lines.push(` Installed to: ${color.dim(context.bmadDir)}`); + } + if (context.modules && context.modules.length > 0) { + lines.push(` Modules: ${color.dim(context.modules.join(', '))}`); + } + if (context.ides && context.ides.length > 0) { + lines.push(` Tools: ${color.dim(context.ides.join(', '))}`); + } + + // Custom/modified file warnings + if (context.customFiles && context.customFiles.length > 0) { + lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`); + } + if (context.modifiedFiles && context.modifiedFiles.length > 0) { + lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`); + } + + await prompts.note(lines.join('\n'), 'BMAD is ready to use!'); + } + /** * Update existing installation */ @@ -1728,6 +1762,7 @@ class Installer { skipModuleInstaller: true, // We'll run it later after IDE setup moduleConfig: moduleConfig, // Pass module config for conditional filtering installer: this, + silent: true, }, ); diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js index 778094043..9bfbdcf30 100644 --- a/tools/cli/installers/lib/ide/_base-ide.js +++ b/tools/cli/installers/lib/ide/_base-ide.js @@ -53,7 +53,7 @@ class BaseIdeSetup { * Cleanup IDE configuration * @param {string} projectDir - Project directory */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { // Default implementation - can be overridden if (this.configDir) { const configPath = path.join(projectDir, this.configDir); @@ -61,7 +61,7 @@ class BaseIdeSetup { const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME); if (await fs.pathExists(bmadRulesPath)) { await fs.remove(bmadRulesPath); - await prompts.log.message(`Removed ${this.name} BMAD configuration`); + if (!options.silent) await prompts.log.message(`Removed ${this.name} BMAD configuration`); } } } diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index e21d4823b..0ab46f5be 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -34,10 +34,10 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { * @returns {Promise} Setup result */ async setup(projectDir, bmadDir, options = {}) { - await prompts.log.info(`Setting up ${this.name}...`); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); // Clean up any old BMAD installation first - await this.cleanup(projectDir); + await this.cleanup(projectDir, options); if (!this.installerConfig) { return { success: false, reason: 'no-config' }; @@ -102,7 +102,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { results.tools = taskToolResult.tools || 0; } - await this.printSummary(results, target_dir); + await this.printSummary(results, target_dir, options); return { success: true, results }; } @@ -439,7 +439,8 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * @param {Object} results - Installation results * @param {string} targetDir - Target directory (relative) */ - async printSummary(results, targetDir) { + async printSummary(results, targetDir, options = {}) { + if (options.silent) return; const parts = []; if (results.agents > 0) parts.push(`${results.agents} agents`); if (results.workflows > 0) parts.push(`${results.workflows} workflows`); @@ -452,14 +453,14 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * Cleanup IDE configuration * @param {string} projectDir - Project directory */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { // Clean all target directories if (this.installerConfig?.targets) { for (const target of this.installerConfig.targets) { - await this.cleanupTarget(projectDir, target.target_dir); + await this.cleanupTarget(projectDir, target.target_dir, options); } } else if (this.installerConfig?.target_dir) { - await this.cleanupTarget(projectDir, this.installerConfig.target_dir); + await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options); } } @@ -468,7 +469,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * @param {string} projectDir - Project directory * @param {string} targetDir - Target directory to clean */ - async cleanupTarget(projectDir, targetDir) { + async cleanupTarget(projectDir, targetDir, options = {}) { const targetPath = path.join(projectDir, targetDir); if (!(await fs.pathExists(targetPath))) { @@ -508,7 +509,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} } } - if (removedCount > 0) { + if (removedCount > 0 && !options.silent) { await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`); } } diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index 4407c558b..541d86aa5 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -42,11 +42,11 @@ class CodexSetup extends BaseIdeSetup { default: 'global', }); - // Display detailed instructions for the chosen option + // Show brief confirmation hint (detailed instructions available via verbose) if (installLocation === 'project') { - await prompts.note(this.getProjectSpecificInstructions(), 'Codex Project Installation'); + await prompts.log.info('Prompts installed to: /.codex/prompts (requires CODEX_HOME)'); } else { - await prompts.note(this.getGlobalInstructions(), 'Codex Global Installation'); + await prompts.log.info('Prompts installed to: ~/.codex/prompts'); } // Confirm the choice @@ -70,7 +70,7 @@ class CodexSetup extends BaseIdeSetup { * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { - await prompts.log.info(`Setting up ${this.name}...`); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); // Always use CLI mode const mode = 'cli'; @@ -82,7 +82,7 @@ class CodexSetup extends BaseIdeSetup { const destDir = this.getCodexPromptDir(projectDir, installLocation); await fs.ensureDir(destDir); - await this.clearOldBmadFiles(destDir); + await this.clearOldBmadFiles(destDir, options); // Collect artifacts and write using underscore format const agentGen = new AgentCommandGenerator(this.bmadFolderName); @@ -122,9 +122,11 @@ class CodexSetup extends BaseIdeSetup { const written = agentCount + workflowCount + tasksWritten; - await prompts.log.success( - `${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} files → ${destDir}`, - ); + if (!options.silent) { + await prompts.log.success( + `${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} files → ${destDir}`, + ); + } return { success: true, @@ -253,7 +255,7 @@ class CodexSetup extends BaseIdeSetup { return written; } - async clearOldBmadFiles(destDir) { + async clearOldBmadFiles(destDir, options = {}) { if (!(await fs.pathExists(destDir))) { return; } @@ -263,7 +265,7 @@ class CodexSetup extends BaseIdeSetup { entries = await fs.readdir(destDir); } catch (error) { // Directory exists but can't be read - skip cleanup - await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`); + if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`); return; } diff --git a/tools/cli/installers/lib/ide/kilo.js b/tools/cli/installers/lib/ide/kilo.js index 43e485f66..2e5734391 100644 --- a/tools/cli/installers/lib/ide/kilo.js +++ b/tools/cli/installers/lib/ide/kilo.js @@ -23,10 +23,10 @@ class KiloSetup extends BaseIdeSetup { * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { - await prompts.log.info(`Setting up ${this.name}...`); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); // Clean up any old BMAD installation first - await this.cleanup(projectDir); + await this.cleanup(projectDir, options); // Load existing config (may contain non-BMAD modes and other settings) const kiloModesPath = path.join(projectDir, this.configFile); @@ -88,9 +88,11 @@ class KiloSetup extends BaseIdeSetup { const taskCount = taskToolCounts.tasks || 0; const toolCount = taskToolCounts.tools || 0; - await prompts.log.success( - `${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`, - ); + if (!options.silent) { + await prompts.log.success( + `${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`, + ); + } return { success: true, @@ -169,7 +171,7 @@ class KiloSetup extends BaseIdeSetup { /** * Cleanup KiloCode configuration */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { const fs = require('fs-extra'); const kiloModesPath = path.join(projectDir, this.configFile); @@ -187,12 +189,12 @@ class KiloSetup extends BaseIdeSetup { if (removedCount > 0) { await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 })); - await prompts.log.message(`Removed ${removedCount} BMAD modes from .kilocodemodes`); + if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .kilocodemodes`); } } } catch { // If parsing fails, leave file as-is - await prompts.log.warn('Warning: Could not parse .kilocodemodes for cleanup'); + if (!options.silent) await prompts.log.warn('Warning: Could not parse .kilocodemodes for cleanup'); } } diff --git a/tools/cli/installers/lib/ide/kiro-cli.js b/tools/cli/installers/lib/ide/kiro-cli.js index eb91c8b22..150dca189 100644 --- a/tools/cli/installers/lib/ide/kiro-cli.js +++ b/tools/cli/installers/lib/ide/kiro-cli.js @@ -18,7 +18,7 @@ class KiroCliSetup extends BaseIdeSetup { * Cleanup old BMAD installation before reinstalling * @param {string} projectDir - Project directory */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { const bmadAgentsDir = path.join(projectDir, this.configDir, this.agentsDir); if (await fs.pathExists(bmadAgentsDir)) { @@ -29,7 +29,7 @@ class KiroCliSetup extends BaseIdeSetup { await fs.remove(path.join(bmadAgentsDir, file)); } } - await prompts.log.message(` Cleaned old BMAD agents from ${this.name}`); + if (!options.silent) await prompts.log.message(` Cleaned old BMAD agents from ${this.name}`); } } @@ -40,9 +40,9 @@ class KiroCliSetup extends BaseIdeSetup { * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { - await prompts.log.info(`Setting up ${this.name}...`); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); - await this.cleanup(projectDir); + await this.cleanup(projectDir, options); const kiroDir = path.join(projectDir, this.configDir); const agentsDir = path.join(kiroDir, this.agentsDir); @@ -52,7 +52,7 @@ class KiroCliSetup extends BaseIdeSetup { // Create BMad agents from source YAML files await this.createBmadAgentsFromSource(agentsDir, projectDir); - await prompts.log.success(`${this.name} configured with BMad agents`); + if (!options.silent) await prompts.log.success(`${this.name} configured with BMad agents`); } /** diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index b4f34aab5..371d1f74f 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -177,11 +177,39 @@ class IdeManager { } try { - await handler.setup(projectDir, bmadDir, options); - return { success: true, ide: ideName }; + const handlerResult = await handler.setup(projectDir, bmadDir, options); + // Build detail string from handler-returned data + let detail = ''; + if (handlerResult && handlerResult.results) { + // Config-driven handlers return { success, results: { agents, workflows, tasks, tools } } + const r = handlerResult.results; + const parts = []; + if (r.agents > 0) parts.push(`${r.agents} agents`); + if (r.workflows > 0) parts.push(`${r.workflows} workflows`); + if (r.tasks > 0) parts.push(`${r.tasks} tasks`); + if (r.tools > 0) parts.push(`${r.tools} tools`); + detail = parts.join(', '); + } else if (handlerResult && handlerResult.counts) { + // Codex handler returns { success, counts: { agents, workflows, tasks }, written } + const c = handlerResult.counts; + const parts = []; + if (c.agents > 0) parts.push(`${c.agents} agents`); + if (c.workflows > 0) parts.push(`${c.workflows} workflows`); + if (c.tasks > 0) parts.push(`${c.tasks} tasks`); + detail = parts.join(', '); + } else if (handlerResult && handlerResult.modes !== undefined) { + // Kilo handler returns { success, modes, workflows, tasks, tools } + const parts = []; + if (handlerResult.modes > 0) parts.push(`${handlerResult.modes} agents`); + if (handlerResult.workflows > 0) parts.push(`${handlerResult.workflows} workflows`); + if (handlerResult.tasks > 0) parts.push(`${handlerResult.tasks} tasks`); + if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`); + detail = parts.join(', '); + } + return { success: true, ide: ideName, detail, handlerResult }; } catch (error) { await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`); - return { success: false, error: error.message }; + return { success: false, ide: ideName, error: error.message }; } } diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index f5317ba75..d5c84cd60 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -298,7 +298,7 @@ class ModuleManager { * @param {string} moduleCode - Code of the module to find (from module.yaml) * @returns {string|null} Path to the module source or null if not found */ - async findModuleSource(moduleCode) { + async findModuleSource(moduleCode, options = {}) { const projectRoot = getProjectRoot(); // First check custom module paths if they exist @@ -315,7 +315,7 @@ class ModuleManager { } // Check external official modules - const externalSource = await this.findExternalModuleSource(moduleCode); + const externalSource = await this.findExternalModuleSource(moduleCode, options); if (externalSource) { return externalSource; } @@ -347,7 +347,7 @@ class ModuleManager { * @param {string} moduleCode - Code of the external module * @returns {string} Path to the cloned repository */ - async cloneExternalModule(moduleCode) { + async cloneExternalModule(moduleCode, options = {}) { const { execSync } = require('node:child_process'); const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode); @@ -357,10 +357,19 @@ class ModuleManager { const cacheDir = this.getExternalCacheDir(); const moduleCacheDir = path.join(cacheDir, moduleCode); + const silent = options.silent || false; // Create cache directory if it doesn't exist await fs.ensureDir(cacheDir); + // Helper to create a spinner or a no-op when silent + const createSpinner = async () => { + if (silent) { + return { start() {}, stop() {}, error() {}, message() {} }; + } + return await prompts.spinner(); + }; + // Track if we need to install dependencies let needsDependencyInstall = false; let wasNewClone = false; @@ -368,13 +377,21 @@ class ModuleManager { // Check if already cloned if (await fs.pathExists(moduleCacheDir)) { // Try to update if it's a git repo - const fetchSpinner = await prompts.spinner(); + const fetchSpinner = await createSpinner(); fetchSpinner.start(`Fetching ${moduleInfo.name}...`); try { const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim(); // Fetch and reset to remote - works better with shallow clones than pull - execSync('git fetch origin --depth 1', { cwd: moduleCacheDir, stdio: 'pipe' }); - execSync('git reset --hard origin/HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }); + execSync('git fetch origin --depth 1', { + cwd: moduleCacheDir, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, + }); + execSync('git reset --hard origin/HEAD', { + cwd: moduleCacheDir, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, + }); const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim(); fetchSpinner.stop(`Fetched ${moduleInfo.name}`); @@ -394,11 +411,12 @@ class ModuleManager { // Clone if not exists or was removed if (wasNewClone) { - const fetchSpinner = await prompts.spinner(); + const fetchSpinner = await createSpinner(); fetchSpinner.start(`Fetching ${moduleInfo.name}...`); try { execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, { - stdio: 'pipe', + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, }); fetchSpinner.stop(`Fetched ${moduleInfo.name}`); } catch (error) { @@ -416,18 +434,18 @@ class ModuleManager { // Force install if we updated or cloned new if (needsDependencyInstall || wasNewClone || nodeModulesMissing) { - const installSpinner = await prompts.spinner(); + const installSpinner = await createSpinner(); installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`); try { execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { cwd: moduleCacheDir, - stdio: 'pipe', + stdio: ['ignore', 'pipe', 'pipe'], timeout: 120_000, // 2 minute timeout }); installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`); } catch (error) { installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`); - await prompts.log.warn(` Warning: ${error.message}`); + if (!silent) await prompts.log.warn(` Warning: ${error.message}`); } } else { // Check if package.json is newer than node_modules @@ -442,18 +460,18 @@ class ModuleManager { } if (packageJsonNewer) { - const installSpinner = await prompts.spinner(); + const installSpinner = await createSpinner(); installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`); try { execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { cwd: moduleCacheDir, - stdio: 'pipe', + stdio: ['ignore', 'pipe', 'pipe'], timeout: 120_000, // 2 minute timeout }); installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`); } catch (error) { installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`); - await prompts.log.warn(` Warning: ${error.message}`); + if (!silent) await prompts.log.warn(` Warning: ${error.message}`); } } } @@ -467,7 +485,7 @@ class ModuleManager { * @param {string} moduleCode - Code of the external module * @returns {string|null} Path to the module source or null if not found */ - async findExternalModuleSource(moduleCode) { + async findExternalModuleSource(moduleCode, options = {}) { const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode); if (!moduleInfo) { @@ -475,7 +493,7 @@ class ModuleManager { } // Clone the external module repo - const cloneDir = await this.cloneExternalModule(moduleCode); + const cloneDir = await this.cloneExternalModule(moduleCode, options); // The module-definition specifies the path to module.yaml relative to repo root // We need to return the directory containing module.yaml @@ -496,7 +514,7 @@ class ModuleManager { * @param {Object} options.logger - Logger instance for output */ async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) { - const sourcePath = await this.findModuleSource(moduleName); + const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); const targetPath = path.join(bmadDir, moduleName); // Check if source module exists @@ -1240,7 +1258,7 @@ class ModuleManager { if (moduleName === 'core') { sourcePath = getSourcePath('core'); } else { - sourcePath = await this.findModuleSource(moduleName); + sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); if (!sourcePath) { // No source found, skip module installer return; diff --git a/tools/cli/lib/prompts.js b/tools/cli/lib/prompts.js index 1dc3b5afd..c70a8e34a 100644 --- a/tools/cli/lib/prompts.js +++ b/tools/cli/lib/prompts.js @@ -99,12 +99,18 @@ async function spinner() { return { start: (msg) => { - spinning = true; - s.start(msg); + if (spinning) { + s.message(msg); + } else { + spinning = true; + s.start(msg); + } }, stop: (msg) => { - spinning = false; - s.stop(msg); + if (spinning) { + spinning = false; + s.stop(msg); + } }, message: (msg) => s.message(msg), error: (msg) => { @@ -264,7 +270,7 @@ async function autocompleteMultiselect(options) { return 'Please select at least one item'; } }, - initialValue: options.initialValues, + initialValue: [...new Set([...(options.initialValues || []), ...(options.lockedValues || [])])], render() { const barColor = this.state === 'error' ? color.yellow : color.cyan; const bar = barColor(clack.S_BAR);