Files
bmad-code-org__bmad-method/tools/cli/lib/cli-utils.js
Davor Racic b1bfce9aa7 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 <noreply@anthropic.com>

* 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>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 00:40:13 -06:00

183 lines
5.6 KiB
JavaScript

const path = require('node:path');
const os = require('node:os');
const prompts = require('./prompts');
const CLIUtils = {
/**
* Get version from package.json
*/
getVersion() {
try {
const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
return packageJson.version || 'Unknown';
} catch {
return 'Unknown';
}
},
/**
* Display BMAD logo using @clack intro + box
* @param {boolean} _clearScreen - Deprecated, ignored (no longer clears screen)
*/
async displayLogo(_clearScreen = true) {
const version = this.getVersion();
const color = await prompts.getColor();
// ASCII art logo
const logo = [
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
' ██████╔╝██╔████╔██║███████║██║ ██║',
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
]
.map((line) => color.yellow(line))
.join('\n');
const tagline = ' Build More, Architect Dreams';
await prompts.box(`${logo}\n${tagline}`, `v${version}`, {
contentAlign: 'center',
rounded: true,
formatBorder: color.blue,
});
},
/**
* Display section header
* @param {string} title - Section title
* @param {string} subtitle - Optional subtitle
*/
async displaySection(title, subtitle = null) {
await prompts.note(subtitle || '', title);
},
/**
* Display info box
* @param {string|Array} content - Content to display
* @param {Object} options - Box options
*/
async displayBox(content, options = {}) {
let text = content;
if (Array.isArray(content)) {
text = content.join('\n\n');
}
const color = await prompts.getColor();
const borderColor = options.borderColor || 'cyan';
const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue };
const formatBorder = colorMap[borderColor] || color.cyan;
await prompts.box(text, options.title, {
rounded: options.borderStyle === 'round' || options.borderStyle === undefined,
formatBorder,
});
},
/**
* Display module configuration header
* @param {string} moduleName - Module name (fallback if no custom header)
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
async displayModuleConfigHeader(moduleName, header = null, subheader = null) {
const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
await prompts.note(subheader || '', title);
},
/**
* Display module with no custom configuration
* @param {string} moduleName - Module name (fallback if no custom header)
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
async displayModuleNoConfig(moduleName, header = null, subheader = null) {
const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`;
await prompts.note(subheader || '', title);
},
/**
* Display step indicator
* @param {number} current - Current step
* @param {number} total - Total steps
* @param {string} description - Step description
*/
async displayStep(current, total, description) {
const progress = `[${current}/${total}]`;
await prompts.log.step(`${progress} ${description}`);
},
/**
* Display completion message
* @param {string} message - Completion message
*/
async displayComplete(message) {
const color = await prompts.getColor();
await prompts.box(`\u2728 ${message}`, 'Complete', {
rounded: true,
formatBorder: color.green,
});
},
/**
* Display error message
* @param {string} message - Error message
*/
async displayError(message) {
const color = await prompts.getColor();
await prompts.box(`\u2717 ${message}`, 'Error', {
rounded: true,
formatBorder: color.red,
});
},
/**
* Format list for display
* @param {Array} items - Items to display
* @param {string} prefix - Item prefix
*/
formatList(items, prefix = '\u2022') {
return items.map((item) => ` ${prefix} ${item}`).join('\n');
},
/**
* Clear previous lines
* @param {number} lines - Number of lines to clear
*/
clearLines(lines) {
for (let i = 0; i < lines; i++) {
process.stdout.moveCursor(0, -1);
process.stdout.clearLine(1);
}
},
/**
* Display module completion message
* @param {string} moduleName - Name of the completed module
* @param {boolean} clearScreen - Whether to clear the screen first (deprecated, always false now)
*/
displayModuleComplete(moduleName, clearScreen = false) {
// No longer clear screen or show boxes - just a simple completion message
// This is deprecated but kept for backwards compatibility
},
/**
* Expand path with ~ expansion
* @param {string} inputPath - Path to expand
* @returns {string} Expanded path
*/
expandPath(inputPath) {
if (!inputPath) return inputPath;
// Expand ~ to home directory
if (inputPath.startsWith('~')) {
return path.join(os.homedir(), inputPath.slice(1));
}
return inputPath;
},
};
module.exports = { CLIUtils };