Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
390 changes: 12 additions & 378 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 0 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,15 @@
"@clack/core": "^1.0.0",
"@clack/prompts": "^1.0.0",
"@kayvan/markdown-tree-parser": "^1.6.1",
"boxen": "^5.1.2",
"chalk": "^4.1.2",
"cli-table3": "^0.6.5",
"commander": "^14.0.0",
"csv-parse": "^6.1.0",
"figlet": "^1.8.0",
"fs-extra": "^11.3.0",
"glob": "^11.0.3",
"ignore": "^7.0.5",
"js-yaml": "^4.1.0",
"ora": "^5.4.1",
"picocolors": "^1.1.1",
"semver": "^7.6.3",
"wrap-ansi": "^7.0.0",
"xml2js": "^0.6.2",
"yaml": "^2.7.0"
},
Expand Down
30 changes: 19 additions & 11 deletions tools/cli/bmad-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ const { program } = require('commander');
const path = require('node:path');
const fs = require('node:fs');
const { execSync } = require('node:child_process');
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) {
const currentLimit = process.stdin.getMaxListeners();
process.stdin.setMaxListeners(Math.max(currentLimit, 50));
}

// Check for updates - do this asynchronously so it doesn't block startup
const packageJson = require('../../package.json');
Expand All @@ -27,17 +35,17 @@ async function checkForUpdate() {
}).trim();

if (result && result !== packageJson.version) {
console.warn('');
console.warn(' ╔═══════════════════════════════════════════════════════════════════════════════╗');
console.warn(' ║ UPDATE AVAILABLE ║');
console.warn(' ║ ║');
console.warn(` ║ You are using version ${packageJson.version} but ${result} is available. ║`);
console.warn(' ║ ║');
console.warn(' ║ To update,exir and first run: ║');
console.warn(` ║ npm cache clean --force && npx bmad-method@${tag} install ║`);
console.warn(' ║ ║');
console.warn(' ╚═══════════════════════════════════════════════════════════════════════════════╝');
console.warn('');
const color = await prompts.getColor();
const updateMsg = [
`You are using version ${packageJson.version} but ${result} is available.`,
'',
'To update, exit and first run:',
` npm cache clean --force && npx bmad-method@${tag} install`,
].join('\n');
await prompts.box(updateMsg, 'Update Available', {
rounded: true,
formatBorder: color.yellow,
});
}
} catch {
// Silently fail - network issues or npm not available
Expand Down
35 changes: 18 additions & 17 deletions tools/cli/commands/install.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const chalk = require('chalk');
const path = require('node:path');
const prompts = require('../lib/prompts');
const { Installer } = require('../installers/lib/core/installer');
const { UI } = require('../lib/ui');

Expand Down Expand Up @@ -30,28 +30,28 @@ module.exports = {
// Set debug flag as environment variable for all components
if (options.debug) {
process.env.BMAD_DEBUG_MANIFEST = 'true';
console.log(chalk.cyan('Debug mode enabled\n'));
await prompts.log.info('Debug mode enabled');
}

const config = await ui.promptInstall(options);

// Handle cancel
if (config.actionType === 'cancel') {
console.log(chalk.yellow('Installation cancelled.'));
await prompts.log.warn('Installation cancelled.');
process.exit(0);
return;
}

// Handle quick update separately
if (config.actionType === 'quick-update') {
const result = await installer.quickUpdate(config);
console.log(chalk.green('\n✨ Quick update complete!'));
console.log(chalk.cyan(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`));
await prompts.log.success('Quick update complete!');
await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`);

// Display version-specific end message
const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader();
messageLoader.displayEndMessage();
await messageLoader.displayEndMessage();

process.exit(0);
return;
Expand All @@ -60,8 +60,8 @@ module.exports = {
// Handle compile agents separately
if (config.actionType === 'compile-agents') {
const result = await installer.compileAgents(config);
console.log(chalk.green('\n✨ Agent recompilation complete!'));
console.log(chalk.cyan(`Recompiled ${result.agentCount} agents with customizations applied`));
await prompts.log.success('Agent recompilation complete!');
await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`);
process.exit(0);
return;
}
Expand All @@ -80,21 +80,22 @@ module.exports = {
// Display version-specific end message from install-messages.yaml
const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader();
messageLoader.displayEndMessage();
await messageLoader.displayEndMessage();

process.exit(0);
}
} catch (error) {
// Check if error has a complete formatted message
if (error.fullMessage) {
console.error(error.fullMessage);
try {
if (error.fullMessage) {
await prompts.log.error(error.fullMessage);
} else {
await prompts.log.error(`Installation failed: ${error.message}`);
}
if (error.stack) {
console.error('\n' + chalk.dim(error.stack));
await prompts.log.message(error.stack);
}
} else {
// Generic error handling for all other errors
console.error(chalk.red('Installation failed:'), error.message);
console.error(chalk.dim(error.stack));
} catch {
console.error(error.fullMessage || error.message || error);
}
process.exit(1);
}
Expand Down
18 changes: 9 additions & 9 deletions tools/cli/commands/status.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const chalk = require('chalk');
const path = require('node:path');
const prompts = require('../lib/prompts');
const { Installer } = require('../installers/lib/core/installer');
const { Manifest } = require('../installers/lib/core/manifest');
const { UI } = require('../lib/ui');
Expand All @@ -21,9 +21,9 @@ module.exports = {
// Check if bmad directory exists
const fs = require('fs-extra');
if (!(await fs.pathExists(bmadDir))) {
console.log(chalk.yellow('No BMAD installation found in the current directory.'));
console.log(chalk.dim(`Expected location: ${bmadDir}`));
console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
await prompts.log.warn('No BMAD installation found in the current directory.');
await prompts.log.message(`Expected location: ${bmadDir}`);
await prompts.log.message('Run "bmad install" to set up a new installation.');
process.exit(0);
return;
}
Expand All @@ -32,8 +32,8 @@ module.exports = {
const manifestData = await manifest._readRaw(bmadDir);

if (!manifestData) {
console.log(chalk.yellow('No BMAD installation manifest found.'));
console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
await prompts.log.warn('No BMAD installation manifest found.');
await prompts.log.message('Run "bmad install" to set up a new installation.');
process.exit(0);
return;
}
Expand All @@ -46,7 +46,7 @@ module.exports = {
const availableUpdates = await manifest.checkForUpdates(bmadDir);

// Display status
ui.displayStatus({
await ui.displayStatus({
installation,
modules,
availableUpdates,
Expand All @@ -55,9 +55,9 @@ module.exports = {

process.exit(0);
} catch (error) {
console.error(chalk.red('Status check failed:'), error.message);
await prompts.log.error(`Status check failed: ${error.message}`);
if (process.env.BMAD_DEBUG) {
console.error(chalk.dim(error.stack));
await prompts.log.message(error.stack);
}
process.exit(1);
}
Expand Down
63 changes: 19 additions & 44 deletions tools/cli/installers/lib/core/config-collector.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
const chalk = require('chalk');
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
const { CLIUtils } = require('../../../lib/cli-utils');
const prompts = require('../../../lib/prompts');
Expand Down Expand Up @@ -260,15 +259,9 @@ class ConfigCollector {

// If module has no config keys at all, handle it specially
if (hasNoConfig && moduleConfig.subheader) {
// Add blank line for better readability (matches other modules)
console.log();
const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;

// Display the module name in color first (matches other modules)
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));

// Show the subheader since there's no configuration to ask about
console.log(chalk.dim(` ✓ ${moduleConfig.subheader}`));
await prompts.log.step(moduleDisplayName);
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
return false; // No new fields
}

Expand Down Expand Up @@ -322,7 +315,7 @@ class ConfigCollector {
}

// Show "no config" message for modules with no new questions (that have config keys)
console.log(chalk.dim(` ${moduleName.toUpperCase()} module already up to date`));
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module already up to date`);
return false; // No new fields
}

Expand Down Expand Up @@ -350,15 +343,15 @@ class ConfigCollector {

if (questions.length > 0) {
// Only show header if we actually have questions
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
console.log(); // Line break before questions
await CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
await prompts.log.message('');
const promptedAnswers = await prompts.prompt(questions);

// Merge prompted answers with static answers
Object.assign(allAnswers, promptedAnswers);
} else if (newStaticKeys.length > 0) {
// Only static fields, no questions - show no config message
console.log(chalk.dim(` ${moduleName.toUpperCase()} module configuration updated`));
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configuration updated`);
}

// Store all answers for cross-referencing
Expand Down Expand Up @@ -588,7 +581,7 @@ class ConfigCollector {

// Skip prompts mode: use all defaults without asking
if (this.skipPrompts) {
console.log(chalk.cyan('Using default configuration for'), chalk.magenta(moduleDisplayName));
await prompts.log.info(`Using default configuration for ${moduleDisplayName}`);
// Use defaults for all questions
for (const question of questions) {
const hasDefault = question.default !== undefined && question.default !== null && question.default !== '';
Expand All @@ -597,12 +590,10 @@ class ConfigCollector {
}
}
} else {
console.log();
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
await prompts.log.step(moduleDisplayName);
let customize = true;
if (moduleName === 'core') {
// Core module: no confirm prompt, so add spacing manually to match visual style
console.log(chalk.gray('│'));
// Core module: no confirm prompt, continues directly
} else {
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
const customizeAnswer = await prompts.prompt([
Expand All @@ -621,7 +612,7 @@ class ConfigCollector {
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');

if (questionsWithoutDefaults.length > 0) {
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
await prompts.log.message(` Asking required questions for ${moduleName.toUpperCase()}...`);
const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
Object.assign(allAnswers, promptedAnswers);
}
Expand Down Expand Up @@ -747,32 +738,15 @@ class ConfigCollector {
const hasNoConfig = actualConfigKeys.length === 0;

if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
// Module explicitly has no configuration - show with special styling
// Add blank line for better readability (matches other modules)
console.log();

// Display the module name in color first (matches other modules)
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));

// Ask user if they want to accept defaults or customize on the next line
const { customize } = await prompts.prompt([
{
type: 'confirm',
name: 'customize',
message: 'Accept Defaults (no to customize)?',
default: true,
},
]);

// Show the subheader if available, otherwise show a default message
await prompts.log.step(moduleDisplayName);
if (moduleConfig.subheader) {
console.log(chalk.dim(` ${moduleConfig.subheader}`));
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
} else {
console.log(chalk.dim(` No custom configuration required`));
await prompts.log.message(` \u2713 No custom configuration required`);
}
} else {
// Module has config but just no questions to ask
console.log(chalk.dim(` ${moduleName.toUpperCase()} module configured`));
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
}
}

Expand Down Expand Up @@ -981,14 +955,15 @@ class ConfigCollector {
}

// Add current value indicator for existing configs
const color = await prompts.getColor();
if (existingValue !== null && existingValue !== undefined) {
if (typeof existingValue === 'boolean') {
message += chalk.dim(` (current: ${existingValue ? 'true' : 'false'})`);
message += color.dim(` (current: ${existingValue ? 'true' : 'false'})`);
} else if (Array.isArray(existingValue)) {
message += chalk.dim(` (current: ${existingValue.join(', ')})`);
message += color.dim(` (current: ${existingValue.join(', ')})`);
} else if (questionType !== 'list') {
// Show the cleaned value (without {project-root}/) for display
message += chalk.dim(` (current: ${existingValue})`);
message += color.dim(` (current: ${existingValue})`);
}
} else if (item.example && questionType === 'input') {
// Show example for input fields
Expand All @@ -998,7 +973,7 @@ class ConfigCollector {
exampleText = this.replacePlaceholders(exampleText, moduleName, moduleConfig);
exampleText = exampleText.replace('{project-root}/', '');
}
message += chalk.dim(` (e.g., ${exampleText})`);
message += color.dim(` (e.g., ${exampleText})`);
}

// Build the question object
Expand Down
Loading
Loading