feat(cli): consolidate installer output to single spinner + summary
Replace ~40 lines of output from 15+ spinner start/stop cycles with a single animated spinner during installation and a final note() summary block showing checkmarks per step. Key changes: - Add results collector pattern in install() method - Replace spinner.stop/start pairs with addResult + spinner.message - Add renderInstallSummary() using prompts.note() with colored output - Propagate silent flag through IDE handlers and module manager - Add spinner race condition guards (start while spinning, stop while stopped) - Add no-op spinner pattern for silent external module cloning - Fix stdin listener limit to be defensive with Math.max - Add GIT_TERMINAL_PROMPT=0 for non-interactive git operations - Merge locked values into initialValue for autocomplete prompts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fd09349683
commit
04edbf6ee3
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
* @returns {Promise<Object>} 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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: <project>/.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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue