Skip to content

Commit b1bfce9

Browse files
dracicclaude
andauthored
refactor: Complete @clack/prompts Migration & Installer Output Consolidation (#1586)
* feat(cli): complete @clack/prompts migration Full migration of BMAD CLI installer from legacy terminal libraries (chalk, ora, boxen, figlet, wrap-ansi, cli-table3, readline) to unified @clack/prompts v1.0.0 visual system. Foundation (prompts.js + cli-utils.js): - Extended prompts.js wrapper with box, spinner, progress, taskLog, path, autocomplete, selectKey, stream, color re-export - Refactored cli-utils.js: displayLogo uses box(), sections use note(), steps use log.step(), removed boxen/figlet/wrap-ansi/cli-table3 UI orchestration (ui.js): - Replaced ~100 console.log+chalk calls with log.*, note(), box() - Replaced ora spinner with @Clack spinner - Module selection: autocompleteMultiselect with locked core module, bulleted post-selection display, maxItems for no-scroll Spinner migration (installer.js): - Replaced 40+ ora spinner calls with @Clack spinner - All spinner.stop() calls include meaningful messages - Failure paths use spinner.error() (red cross) instead of stop() Readline migration (agent/installer.js + config-collector.js): - Migrated readline prompts to @Clack text/confirm/select - Fixed chalk.dim bug (chalk was never imported) - Removed chalk from config-collector.js IDE handlers + modules (7 files): - Replaced chalk+ora across all IDE handlers and module manager - Fixed options.installer undefined bug in manager.js update() Cleanup: - Removed ora, boxen, figlet, wrap-ansi, cli-table3 from dependencies - chalk stays (used outside tools/cli/ scope) - Replaced hand-drawn Unicode update box in bmad-cli.js with box() - Added process.stdin.setMaxListeners(25) for sequential prompts Spinner wrapper adds isSpinning state tracking (not native to @Clack). Removed dead groupMultiselect and sortKey sort calls. Ref: tech-spec-installer-clack-migration-ui-enhancement.md Co-Authored-By: Claude Opus 4.6 <[email protected]> * 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 <[email protected]> * fix(cli): resolve code review findings from @clack/prompts migration Address 31 issues across 14 CLI files found during PR #1586 review (Augment Code + CodeRabbit): - Fix bmadDir ReferenceError by hoisting declaration before try block - Wrap console.log monkey-patch in try/finally for safe restoration - Fix transformWorkflowPath dead code and undefined return path - Fix broken symlink crash in _config-driven.js and codex.js cleanup - Pass installer instance through update() for agent recompilation - Fix @clack/prompts API: defaultValue→default, initialValue→default - Use nullish coalescing (??) instead of logical OR for falsy values - Forward options in recursive promptToolSelection calls - Remove no-op replaceAll('_bmad','_bmad') in manager and generator - Remove unused confirm prompt in config-collector hasNoConfig branch - Guard spinner.message() when spinner is not running - Add missing methods to silent spinner stub (cancel, clear, isSpinning) - Wrap install.js error handler with inner try/catch + console fallback - Gate codex per-entry error log with silent flag - Add return statements to all stream wrapper methods - Remove dead variables (availableNames, hasCustomContentItems) - Filter core module from update flow selection - Replace borderColor ternary chain with object map - Fix Kilo "agents" label to "modes" in IDE manager - Normalize error return shape for unsupported IDEs - Fix spinner message timing before dependency resolution - Guard undefined moduleDir in dependency-resolver - Fix workflowsInstalled counter inflation in custom handler - Migrate console.warn calls to prompts.log.warn - Replace console.log() with prompts.log.message('') - Fix legacyBmadPath hardcoded to .bmad instead of entry.name - Fix focusedValue never assigned breaking SPACE toggle and hints Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent ecf7fbc commit b1bfce9

24 files changed

+1047
-1362
lines changed

package-lock.json

Lines changed: 12 additions & 378 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,20 +68,15 @@
6868
"@clack/core": "^1.0.0",
6969
"@clack/prompts": "^1.0.0",
7070
"@kayvan/markdown-tree-parser": "^1.6.1",
71-
"boxen": "^5.1.2",
7271
"chalk": "^4.1.2",
73-
"cli-table3": "^0.6.5",
7472
"commander": "^14.0.0",
7573
"csv-parse": "^6.1.0",
76-
"figlet": "^1.8.0",
7774
"fs-extra": "^11.3.0",
7875
"glob": "^11.0.3",
7976
"ignore": "^7.0.5",
8077
"js-yaml": "^4.1.0",
81-
"ora": "^5.4.1",
8278
"picocolors": "^1.1.1",
8379
"semver": "^7.6.3",
84-
"wrap-ansi": "^7.0.0",
8580
"xml2js": "^0.6.2",
8681
"yaml": "^2.7.0"
8782
},

tools/cli/bmad-cli.js

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ const { program } = require('commander');
22
const path = require('node:path');
33
const fs = require('node:fs');
44
const { execSync } = require('node:child_process');
5+
const prompts = require('./lib/prompts');
6+
7+
// The installer flow uses many sequential @clack/prompts, each adding keypress
8+
// listeners to stdin. Raise the limit to avoid spurious EventEmitter warnings.
9+
if (process.stdin?.setMaxListeners) {
10+
const currentLimit = process.stdin.getMaxListeners();
11+
process.stdin.setMaxListeners(Math.max(currentLimit, 50));
12+
}
513

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

2937
if (result && result !== packageJson.version) {
30-
console.warn('');
31-
console.warn(' ╔═══════════════════════════════════════════════════════════════════════════════╗');
32-
console.warn(' ║ UPDATE AVAILABLE ║');
33-
console.warn(' ║ ║');
34-
console.warn(` ║ You are using version ${packageJson.version} but ${result} is available. ║`);
35-
console.warn(' ║ ║');
36-
console.warn(' ║ To update,exir and first run: ║');
37-
console.warn(` ║ npm cache clean --force && npx bmad-method@${tag} install ║`);
38-
console.warn(' ║ ║');
39-
console.warn(' ╚═══════════════════════════════════════════════════════════════════════════════╝');
40-
console.warn('');
38+
const color = await prompts.getColor();
39+
const updateMsg = [
40+
`You are using version ${packageJson.version} but ${result} is available.`,
41+
'',
42+
'To update, exit and first run:',
43+
` npm cache clean --force && npx bmad-method@${tag} install`,
44+
].join('\n');
45+
await prompts.box(updateMsg, 'Update Available', {
46+
rounded: true,
47+
formatBorder: color.yellow,
48+
});
4149
}
4250
} catch {
4351
// Silently fail - network issues or npm not available

tools/cli/commands/install.js

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
const chalk = require('chalk');
21
const path = require('node:path');
2+
const prompts = require('../lib/prompts');
33
const { Installer } = require('../installers/lib/core/installer');
44
const { UI } = require('../lib/ui');
55

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

3636
const config = await ui.promptInstall(options);
3737

3838
// Handle cancel
3939
if (config.actionType === 'cancel') {
40-
console.log(chalk.yellow('Installation cancelled.'));
40+
await prompts.log.warn('Installation cancelled.');
4141
process.exit(0);
4242
return;
4343
}
4444

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

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

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

8585
process.exit(0);
8686
}
8787
} catch (error) {
88-
// Check if error has a complete formatted message
89-
if (error.fullMessage) {
90-
console.error(error.fullMessage);
88+
try {
89+
if (error.fullMessage) {
90+
await prompts.log.error(error.fullMessage);
91+
} else {
92+
await prompts.log.error(`Installation failed: ${error.message}`);
93+
}
9194
if (error.stack) {
92-
console.error('\n' + chalk.dim(error.stack));
95+
await prompts.log.message(error.stack);
9396
}
94-
} else {
95-
// Generic error handling for all other errors
96-
console.error(chalk.red('Installation failed:'), error.message);
97-
console.error(chalk.dim(error.stack));
97+
} catch {
98+
console.error(error.fullMessage || error.message || error);
9899
}
99100
process.exit(1);
100101
}

tools/cli/commands/status.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
const chalk = require('chalk');
21
const path = require('node:path');
2+
const prompts = require('../lib/prompts');
33
const { Installer } = require('../installers/lib/core/installer');
44
const { Manifest } = require('../installers/lib/core/manifest');
55
const { UI } = require('../lib/ui');
@@ -21,9 +21,9 @@ module.exports = {
2121
// Check if bmad directory exists
2222
const fs = require('fs-extra');
2323
if (!(await fs.pathExists(bmadDir))) {
24-
console.log(chalk.yellow('No BMAD installation found in the current directory.'));
25-
console.log(chalk.dim(`Expected location: ${bmadDir}`));
26-
console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
24+
await prompts.log.warn('No BMAD installation found in the current directory.');
25+
await prompts.log.message(`Expected location: ${bmadDir}`);
26+
await prompts.log.message('Run "bmad install" to set up a new installation.');
2727
process.exit(0);
2828
return;
2929
}
@@ -32,8 +32,8 @@ module.exports = {
3232
const manifestData = await manifest._readRaw(bmadDir);
3333

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

4848
// Display status
49-
ui.displayStatus({
49+
await ui.displayStatus({
5050
installation,
5151
modules,
5252
availableUpdates,
@@ -55,9 +55,9 @@ module.exports = {
5555

5656
process.exit(0);
5757
} catch (error) {
58-
console.error(chalk.red('Status check failed:'), error.message);
58+
await prompts.log.error(`Status check failed: ${error.message}`);
5959
if (process.env.BMAD_DEBUG) {
60-
console.error(chalk.dim(error.stack));
60+
await prompts.log.message(error.stack);
6161
}
6262
process.exit(1);
6363
}

tools/cli/installers/lib/core/config-collector.js

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const path = require('node:path');
22
const fs = require('fs-extra');
33
const yaml = require('yaml');
4-
const chalk = require('chalk');
54
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
65
const { CLIUtils } = require('../../../lib/cli-utils');
76
const prompts = require('../../../lib/prompts');
@@ -260,15 +259,9 @@ class ConfigCollector {
260259

261260
// If module has no config keys at all, handle it specially
262261
if (hasNoConfig && moduleConfig.subheader) {
263-
// Add blank line for better readability (matches other modules)
264-
console.log();
265262
const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
266-
267-
// Display the module name in color first (matches other modules)
268-
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
269-
270-
// Show the subheader since there's no configuration to ask about
271-
console.log(chalk.dim(` ✓ ${moduleConfig.subheader}`));
263+
await prompts.log.step(moduleDisplayName);
264+
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
272265
return false; // No new fields
273266
}
274267

@@ -322,7 +315,7 @@ class ConfigCollector {
322315
}
323316

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

@@ -350,15 +343,15 @@ class ConfigCollector {
350343

351344
if (questions.length > 0) {
352345
// Only show header if we actually have questions
353-
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
354-
console.log(); // Line break before questions
346+
await CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
347+
await prompts.log.message('');
355348
const promptedAnswers = await prompts.prompt(questions);
356349

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

364357
// Store all answers for cross-referencing
@@ -588,7 +581,7 @@ class ConfigCollector {
588581

589582
// Skip prompts mode: use all defaults without asking
590583
if (this.skipPrompts) {
591-
console.log(chalk.cyan('Using default configuration for'), chalk.magenta(moduleDisplayName));
584+
await prompts.log.info(`Using default configuration for ${moduleDisplayName}`);
592585
// Use defaults for all questions
593586
for (const question of questions) {
594587
const hasDefault = question.default !== undefined && question.default !== null && question.default !== '';
@@ -597,12 +590,10 @@ class ConfigCollector {
597590
}
598591
}
599592
} else {
600-
console.log();
601-
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
593+
await prompts.log.step(moduleDisplayName);
602594
let customize = true;
603595
if (moduleName === 'core') {
604-
// Core module: no confirm prompt, so add spacing manually to match visual style
605-
console.log(chalk.gray('│'));
596+
// Core module: no confirm prompt, continues directly
606597
} else {
607598
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
608599
const customizeAnswer = await prompts.prompt([
@@ -621,7 +612,7 @@ class ConfigCollector {
621612
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
622613

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

749740
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
750-
// Module explicitly has no configuration - show with special styling
751-
// Add blank line for better readability (matches other modules)
752-
console.log();
753-
754-
// Display the module name in color first (matches other modules)
755-
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
756-
757-
// Ask user if they want to accept defaults or customize on the next line
758-
const { customize } = await prompts.prompt([
759-
{
760-
type: 'confirm',
761-
name: 'customize',
762-
message: 'Accept Defaults (no to customize)?',
763-
default: true,
764-
},
765-
]);
766-
767-
// Show the subheader if available, otherwise show a default message
741+
await prompts.log.step(moduleDisplayName);
768742
if (moduleConfig.subheader) {
769-
console.log(chalk.dim(` ${moduleConfig.subheader}`));
743+
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
770744
} else {
771-
console.log(chalk.dim(` No custom configuration required`));
745+
await prompts.log.message(` \u2713 No custom configuration required`);
772746
}
773747
} else {
774748
// Module has config but just no questions to ask
775-
console.log(chalk.dim(` ${moduleName.toUpperCase()} module configured`));
749+
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
776750
}
777751
}
778752

@@ -981,14 +955,15 @@ class ConfigCollector {
981955
}
982956

983957
// Add current value indicator for existing configs
958+
const color = await prompts.getColor();
984959
if (existingValue !== null && existingValue !== undefined) {
985960
if (typeof existingValue === 'boolean') {
986-
message += chalk.dim(` (current: ${existingValue ? 'true' : 'false'})`);
961+
message += color.dim(` (current: ${existingValue ? 'true' : 'false'})`);
987962
} else if (Array.isArray(existingValue)) {
988-
message += chalk.dim(` (current: ${existingValue.join(', ')})`);
963+
message += color.dim(` (current: ${existingValue.join(', ')})`);
989964
} else if (questionType !== 'list') {
990965
// Show the cleaned value (without {project-root}/) for display
991-
message += chalk.dim(` (current: ${existingValue})`);
966+
message += color.dim(` (current: ${existingValue})`);
992967
}
993968
} else if (item.example && questionType === 'input') {
994969
// Show example for input fields
@@ -998,7 +973,7 @@ class ConfigCollector {
998973
exampleText = this.replacePlaceholders(exampleText, moduleName, moduleConfig);
999974
exampleText = exampleText.replace('{project-root}/', '');
1000975
}
1001-
message += chalk.dim(` (e.g., ${exampleText})`);
976+
message += color.dim(` (e.g., ${exampleText})`);
1002977
}
1003978

1004979
// Build the question object

0 commit comments

Comments
 (0)