Skip to content

Conversation

@dracic
Copy link
Contributor

@dracic dracic commented Feb 7, 2026

Summary

  • Migrate the entire BMAD CLI from a patchwork of ora, chalk+console.log, boxen, readline, and figlet to a unified @clack/prompts visual system across all 24 modified files
  • Replace ~40 lines of installer output (15+ spinner start/stop cycles) with a single animated spinner and a final summary note block
  • Remove 5 unused dependencies (ora, boxen, figlet, wrap-ansi, cli-table3), saving ~870 lines from the lockfile

What changed

prompts.js wrapper -- new @Clack v1.0.0 features

Extended with intro(), outro(), cancel(), note(), box(), spinner(), progress(), tasks(), taskLog(), path(), autocomplete(), selectKey(), stream.*, color re-export, and a full log.* object. Added spinner race condition guards (start-while-spinning becomes message(), stop-while-stopped is a no-op) and merged locked values into initialValue for autocomplete prompts.

cli-utils.js -- display primitives refactored

Replaced boxen-based boxes with prompts.box(), chalk section headers with prompts.note(), and chalk step indicators with prompts.log.step(). Removed dead figlet import, unused displayTable() function, and boxen/wrapAnsi/cli-table3 requires.

ui.js -- main UI orchestrator

Replaced ~100 console.log(chalk.*) calls with prompts.log.*, prompts.note(), and prompts.box(). Replaced the single ora spinner with @Clack spinner. Replaced legacy installation warning blocks and version displays with note(). Removed chalk and ora imports entirely. All display functions made async.

installer.js -- ora spinner replacement + output consolidation

Replaced 40+ ora spinner operations with @Clack spinner equivalents. Converted all spinner.text assignments to s.message(), all spinner.succeed() to s.stop(), and spinner.fail() to s.error(). Replaced ~30 console.log(chalk.*) calls with prompts.log.* and prompts.note().

Refactored install() to use a results collector pattern: one spinner.start() at the beginning, spinner.message() for phase updates, addResult(step, status, detail) to accumulate results, and a single spinner.stop() at the end followed by renderInstallSummary() which renders a prompts.note() block with colored checkmarks per step.

agent/installer.js -- readline removal

Fully migrated from Node.js readline to @Clack prompts (text(), confirm(), select()). Fixed a pre-existing bug where chalk.dim() was called but chalk was never imported.

config-collector.js + dependency-resolver.js -- chalk removal

Replaced all chalk console.log output with prompts.log.*. Preserved verbose gating.

IDE handlers -- silent flag + chalk removal

Migrated all chalk output in codex.js, kilo.js, kiro-cli.js, _config-driven.js, _base-ide.js, and manager.js (IDE) to prompts.log.*. Added silent flag propagation through all handlers to suppress per-handler log output while the main installer spinner is active. Config-driven IDEs that need no prompting are marked with { _noConfigNeeded: true } so the spinner runs uninterrupted. IDEs requiring interactive configuration (e.g., Codex install location) stop the spinner, collect input, then restart it.

modules/manager.js -- ora replacement + silent mode

Replaced ora spinners with @Clack spinner. Added no-op spinner pattern for silent mode. Added GIT_TERMINAL_PROMPT=0 and stdio: ['ignore', 'pipe', 'pipe'] to git execSync calls to prevent hangs.

Entry points + minor files

Replaced hand-drawn Unicode update box in bmad-cli.js with prompts.box(). Made stdin.setMaxListeners defensive with Math.max(currentLimit, 50). Migrated chalk output in install.js, status.js, message-loader.js, and custom/handler.js.

Dependency cleanup

Removed 5 dependencies from package.json. chalk stays (used in files outside CLI scope).

Package Was used in Replaced by
[email protected] installer.js (40+), modules/manager.js, ui.js @clack/prompts spinner
[email protected] cli-utils.js @clack/prompts box()
[email protected] cli-utils.js (dead import) Removed
[email protected] cli-utils.js @Clack handles wrapping internally
[email protected] cli-utils.js (zero callers) Removed

Files changed (24)

File Change
tools/cli/lib/prompts.js +228 -- new @Clack wrappers, spinner guards, color re-export
tools/cli/lib/cli-utils.js Refactored all display functions to @Clack, removed 5 requires
tools/cli/lib/ui.js Replaced ~100 chalk calls, removed ora/chalk imports
tools/cli/installers/lib/core/installer.js Replaced 40+ ora calls, added results collector + renderInstallSummary
tools/cli/installers/lib/core/config-collector.js Replaced chalk with prompts.log.*
tools/cli/installers/lib/core/dependency-resolver.js Replaced chalk with prompts.log.*
tools/cli/installers/lib/modules/manager.js Replaced ora+chalk, added silent mode, fixed git stdio
tools/cli/installers/lib/ide/_config-driven.js Added silent flag, replaced chalk
tools/cli/installers/lib/ide/_base-ide.js Added silent flag to cleanup
tools/cli/installers/lib/ide/codex.js Added silent flag, replaced chalk+note output
tools/cli/installers/lib/ide/kilo.js Added silent flag, replaced chalk
tools/cli/installers/lib/ide/kiro-cli.js Added silent flag, replaced chalk
tools/cli/installers/lib/ide/manager.js Captures handler results, builds detail strings
tools/cli/lib/agent/installer.js Full readline-to-@Clack migration, fixed chalk.dim bug
tools/cli/bmad-cli.js Replaced Unicode box with prompts.box(), defensive setMaxListeners
tools/cli/commands/install.js Replaced chalk with prompts.log.*
tools/cli/commands/status.js Replaced chalk with prompts.log.*
tools/cli/installers/lib/custom/handler.js Replaced chalk with prompts.log.*
tools/cli/installers/lib/message-loader.js Replaced chalk with prompts.log.*
tools/cli/installers/lib/ide/shared/agent-command-generator.js Removed unused chalk require
tools/cli/installers/lib/ide/shared/task-tool-command-generator.js Removed unused chalk require
tools/cli/installers/lib/ide/shared/workflow-command-generator.js Replaced chalk with prompts.log.*
package.json Removed 5 dependencies
package-lock.json -870 lines

Test plan

  • npm test -- all 52 schema tests, 12 install tests, 10 agent validations pass
  • Lint clean on all modified files
  • Manual: npx bmad install in clean directory -- verify single spinner + summary note
  • Manual: Install with IDE requiring prompts (Codex) -- verify spinner stops/restarts around prompts
  • Manual: Ctrl+C during spinner -- verify clean terminal restore
  • Manual: npx bmad install --yes -- verify non-interactive mode works
  • Manual: Windows Terminal, PowerShell, CMD.exe -- verify Unicode renders correctly

dracic and others added 2 commits February 7, 2026 20:27
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]>
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]>
@dracic
Copy link
Contributor Author

dracic commented Feb 7, 2026

@bmadcode this requires more detailed testing due to the large volume of changes, so I'm not sure if I would push it for the v6 release if the current setup is stable enough. On the other hand, this is one of the options in the discussion regarding the final solution for the installer.

@augmentcode
Copy link

augmentcode bot commented Feb 7, 2026

🤖 Augment PR Summary

Summary: This PR completes a broad CLI UX refactor by standardizing interactive output on @clack/prompts and removing legacy spinner/box/log utilities.

Changes:

  • Expands tools/cli/lib/prompts.js into a richer wrapper (intro/outro/note/box/spinner/progress/tasks, logging helpers, path/autocomplete/selectKey, stream helpers, and spinner state guards).
  • Migrates CLI display primitives (cli-utils.js) and the main UI flow (ui.js) from chalk/console.log/boxen/ora to prompts.log, prompts.note(), and prompts.box().
  • Refactors the core installer to use an @Clack spinner plus a consolidated end-of-run summary note block, replacing many prior spinner start/stop cycles.
  • Updates IDE handlers to accept a silent option so per-handler logging can be suppressed while the main installer spinner is running.
  • Migrates the agent installer from Node readline to @Clack prompts for consistent interactivity.
  • Makes message loader APIs async and updates call sites accordingly.
  • Removes unused dependencies (ora, boxen, figlet, wrap-ansi, cli-table3) and trims the lockfile significantly.

Technical Notes: The PR relies on async display helpers throughout the CLI, adds stdin listener-limit protection, and centralizes color output via the prompts color utilities (picocolors).

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 4 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

Removed five npm dependencies (boxen, cli-table3, figlet, ora, wrap-ansi) and refactored the CLI to use a centralized prompts-based logging system instead of chalk, ora, boxen, and direct console output. Updated ~25 files across installers, IDE setup, and CLI utilities to route messages through async prompts APIs.

Changes

Cohort / File(s) Summary
Dependency Removal
package.json
Removed five runtime dependencies: boxen, cli-table3, figlet, ora, and wrap-ansi.
Core CLI Refactoring
tools/cli/bmad-cli.js, tools/cli/commands/install.js, tools/cli/commands/status.js, tools/cli/lib/agent/installer.js
Replaced console/chalk logging with prompts-based async logging; updated update-check banners and user prompts to use prompts.box/log; made message display methods async.
Prompts Infrastructure
tools/cli/lib/prompts.js, tools/cli/lib/cli-utils.js, tools/cli/lib/ui.js
Expanded prompts module with new utilities (cancel, box, progress, taskLog, path, autocomplete, selectKey, stream, getColor); enhanced spinner with state tracking and locked-value support for multiselect; converted cli-utils display methods to async prompts-based rendering; refactored UI class methods to async and prompts-driven output.
Installer Core & Module Management
tools/cli/installers/lib/core/config-collector.js, tools/cli/installers/lib/core/dependency-resolver.js, tools/cli/installers/lib/core/installer.js, tools/cli/installers/lib/modules/manager.js
Replaced chalk/console with prompts logging; made reportResults and internal methods async; added new renderInstallSummary method to Installer; introduced options.silent support and spinner-based progress tracking in module discovery.
Custom Installer & Handler
tools/cli/installers/lib/custom/handler.js, tools/cli/installers/lib/ide/manager.js
Replaced chalk with prompts logging for YAML errors and verbose output; made loadCustomInstallerFiles async; enhanced setup return payload with detail and handlerResult; propagated options through loading and setup flows.
IDE Setup Classes
tools/cli/installers/lib/ide/_base-ide.js, tools/cli/installers/lib/ide/_config-driven.js, tools/cli/installers/lib/ide/codex.js, tools/cli/installers/lib/ide/kilo.js, tools/cli/installers/lib/ide/kiro-cli.js
Updated method signatures to accept optional options parameter (with default empty object) and async printSummary/cleanup methods; added options.silent support; replaced chalk warnings/success with prompts-based logging; updated clearOldBmadFiles and related cleanup to respect silent mode.
Shared IDE Utilities
tools/cli/installers/lib/ide/shared/agent-command-generator.js, tools/cli/installers/lib/ide/shared/task-tool-command-generator.js, tools/cli/installers/lib/ide/shared/workflow-command-generator.js
Removed unused chalk import; replaced chalk-based logging with prompts.log.warn for workflow manifest warnings.
Message & Utility Updates
tools/cli/installers/lib/message-loader.js
Made displayStartMessage and displayEndMessage async; replaced chalk logging with prompts.log.info for message output.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #1557: Modifies autocompleteMultiselect input handling in tools/cli/lib/prompts.js with lock-aware SPACE key behavior changes that interact with this PR's locked-values feature.
  • PR #1316: Large-scale CLI prompting layer migration from inquirer/chalk/ora/boxen to centralized prompts API, overlapping broadly with this PR's UI refactoring.
  • PR #1492: Overlaps on multiple installer files (dependency-resolver.js, IDE modules) with similar logging refactoring patterns and options parameter propagation.

Suggested reviewers

  • bmadcode
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main refactoring effort: migrating to @clack/prompts and consolidating installer output across the CLI.
Description check ✅ Passed The description comprehensively explains the refactoring changes, including summary, detailed file-by-file changes, dependency removals, and test plan.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (15)
tools/cli/installers/lib/ide/shared/workflow-command-generator.js (2)

234-250: ⚠️ Potential issue | 🟠 Major

transformWorkflowPath never handles /src/core/ paths and returns undefined for non-bmm inputs.

The else if (workflowPath.includes('/src/core/')) on Line 241 is nested inside the if (workflowPath.includes('/src/bmm/')) block. When the path contains /src/bmm/, the regex on Line 238 will always match, making the else if dead code. When the path does not contain /src/bmm/, the entire block is skipped and the function returns undefined.

This means any workflow path referencing /src/core/ is silently dropped, and callers receive undefined instead of a transformed path—which will propagate as the string "undefined" wherever it's interpolated into template content (e.g., Line 213 in buildLauncherContent).

Proposed fix
   transformWorkflowPath(workflowPath) {
     let transformed = workflowPath;
 
     if (workflowPath.includes('/src/bmm/')) {
       const match = workflowPath.match(/\/src\/bmm\/(.+)/);
       if (match) {
         transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`;
       }
-    } else if (workflowPath.includes('/src/core/')) {
-      const match = workflowPath.match(/\/src\/core\/(.+)/);
+    } else if (workflowPath.includes('/src/core/')) {
+      const match = workflowPath.match(/\/src\/core\/(.+)/);
       if (match) {
         transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`;
       }
-
-      return transformed;
     }
+
+    return transformed;
   }

155-161: ⚠️ Potential issue | 🟠 Major

Line 161 is a no-op and should be removed or corrected.

Line 161 replaces '_bmad' with '_bmad'—the search and replacement strings are identical (verified at byte level: 0x5f 0x62 0x6d 0x61 0x64). This has no effect after Line 160 already replaces '_bmad' with this.bmadFolderName. Either this line should be deleted, or the search/replacement strings differ in a way not visible in the code (e.g., different unicode characters that should be preserved).

The parallel code in tools/cli/installers/lib/modules/manager.js lines 792–793 shows the same pattern but with reversed order, and includes a comment "IMPORTANT: Replace escape sequence and placeholder" that contradicts the identical strings shown in the code. This suggests a systematic issue where escape sequences may not be preserved in the codebase.

tools/cli/installers/lib/custom/handler.js (1)

275-278: ⚠️ Potential issue | 🟡 Minor

Pre-existing: workflowsInstalled is incremented for preserved files too.

This counter runs unconditionally for every .md file, regardless of whether the file was preserved (Line 247-249) or newly copied (Line 250-268). This means the count inflates on reinstall/update. Not introduced by this PR, but it's in the changed file surface.

tools/cli/commands/install.js (1)

87-100: ⚠️ Potential issue | 🟠 Major

Incomplete migration: console.error on Line 90, and fragile await chains in catch block.

Two issues here:

  1. Line 90 still uses console.error(error.fullMessage) while every other logging call in this file migrated to prompts.log.*. If the intent was to keep raw output for pre-formatted messages, that should be documented. Otherwise this is an oversight.

  2. Lines 92, 96–97 await async prompts calls inside a catch block. If prompts.log.* itself throws (e.g., @clack/prompts fails to lazy-load), the original error context is silently lost and replaced by the prompts failure. A defensive try/catch or a fallback to console.error would be safer here.

Proposed fix
     } catch (error) {
-      // Check if error has a complete formatted message
-      if (error.fullMessage) {
-        console.error(error.fullMessage);
-        if (error.stack) {
-          await prompts.log.message(error.stack);
-        }
-      } else {
-        // Generic error handling for all other errors
-        await prompts.log.error(`Installation failed: ${error.message}`);
-        await prompts.log.message(error.stack);
+      try {
+        if (error.fullMessage) {
+          await prompts.log.error(error.fullMessage);
+          if (error.stack) {
+            await prompts.log.message(error.stack);
+          }
+        } else {
+          await prompts.log.error(`Installation failed: ${error.message}`);
+          await prompts.log.message(error.stack);
+        }
+      } catch {
+        // Fallback if prompts logging itself fails
+        console.error(error.fullMessage || `Installation failed: ${error.message}`);
+        if (error.stack) console.error(error.stack);
       }
       process.exit(1);
     }
tools/cli/installers/lib/modules/manager.js (2)

786-793: ⚠️ Potential issue | 🔴 Critical

Remove the no-op replaceAll call and fix ordering inconsistency with workflow-command-generator.js.

Lines 792–793 contain a problematic pattern: Line 792 replaceAll('_bmad', '_bmad') is a literal no-op that serves no purpose. More critically, the ordering of operations differs between this file and workflow-command-generator.js (lines 160–161): here the no-op comes first followed by the actual replacement, but in the other file it's reversed (actual replacement first, no-op second). One of these orderings is wrong. Remove the no-op call on line 792 and align the replacement sequence with workflow-command-generator.js.


288-291: ⚠️ Potential issue | 🟡 Minor

Migrate remaining console.warn calls to prompts.log.warn for consistency.

The file has migrated nearly all logging to prompts.log.* but three console.warn calls remain in error handlers at lines 290, 697, and 1326. Replace them with prompts.log.warn to maintain consistent logging throughout the module.

tools/cli/bmad-cli.js (1)

17-19: ⚠️ Potential issue | 🟡 Minor

Fire-and-forget async update check can interleave output with command results.

checkForUpdate() is invoked at module-load time and forgotten. Because prompts.box() is now async and writes to stdout, the update banner can render in the middle of (or after) any command's output — especially fast commands like status. The old console.warn approach had the same theoretical race, but the new await prompts.box(...) internally awaits rendering, meaning the promise chain takes longer and the interleaving window is wider.

Consider capturing the update message and printing it at a deterministic point (e.g., after program.parseAsync() or via a Commander hook) rather than fire-and-forget.

Also applies to: 38-48

tools/cli/installers/lib/ide/_config-driven.js (1)

494-510: ⚠️ Potential issue | 🟠 Major

Unhandled fs.stat error on broken symlinks will crash cleanup loop.

If an entry starting with bmad is a broken symlink, fs.stat(entryPath) throws ENOENT and the entire cleanupTarget aborts, leaving remaining files uncleaned. Wrap the per-entry block in a try/catch to make cleanup resilient.

Also, the isFile() / isDirectory() branch is redundant — both paths call fs.remove(entryPath). Simplify.

Proposed fix
     for (const entry of entries) {
       if (!entry || typeof entry !== 'string') {
         continue;
       }
       if (entry.startsWith('bmad')) {
-        const entryPath = path.join(targetPath, entry);
-        const stat = await fs.stat(entryPath);
-        if (stat.isFile()) {
-          await fs.remove(entryPath);
-          removedCount++;
-        } else if (stat.isDirectory()) {
+        try {
+          const entryPath = path.join(targetPath, entry);
           await fs.remove(entryPath);
           removedCount++;
+        } catch {
+          // Broken symlink or permission error — skip and continue
         }
       }
     }
tools/cli/lib/ui.js (2)

828-828: ⚠️ Potential issue | 🟡 Minor

Dead variable hasCustomContentItems.

hasCustomContentItems is declared as false on line 828 and never read or reassigned anywhere in the method. Remove it.


558-564: ⚠️ Potential issue | 🟠 Major

Recursive promptToolSelection call drops the options parameter.

When the user declines "no tools selected" and the method recurses, options is not forwarded. This means command-line flags (e.g. --tools) are lost on retry, which could cause unexpected behavior (e.g., re-prompting interactively when CLI args were supplied).

Same issue on line 642.

Proposed fix
         if (!confirmNoTools) {
-          return this.promptToolSelection(projectDir);
+          return this.promptToolSelection(projectDir, options);
         }

And similarly at line 642:

         if (!confirmNoTools) {
-          return this.promptToolSelection(projectDir);
+          return this.promptToolSelection(projectDir, options);
         }
tools/cli/installers/lib/core/config-collector.js (1)

740-759: ⚠️ Potential issue | 🟠 Major

"Accept Defaults?" prompt result is captured but never used.

For modules with zero configuration keys (line 738: hasNoConfig === true), the code prompts the user with "Accept Defaults (no to customize)?" on line 749, destructures the result into customize, and then unconditionally displays the subheader/message regardless of the answer. The user's choice has no effect — there's nothing to customize.

Either remove the prompt entirely (since there's nothing to configure) or wire the customize response to a meaningful branch.

tools/cli/installers/lib/core/dependency-resolver.js (1)

80-96: ⚠️ Potential issue | 🟡 Minor

moduleDir is undefined when the module is neither 'core' nor 'bmm', producing a misleading warning.

If selectedModules contains a module other than 'core' or 'bmm' (and it isn't filtered by isExternalModule), the moduleDir variable is never assigned. Line 93 then calls fs.pathExists(undefined) which returns false, and line 94 logs "Module directory not found: undefined". This is confusing and a latent bug.

While pre-existing, the new prompts.log.warn makes this more user-visible. Consider adding an explicit guard:

🛡️ Proposed guard
+      if (!moduleDir) {
+        if (options.verbose) {
+          await prompts.log.warn(`No source mapping for module: ${module}`);
+        }
+        continue;
+      }
+
       if (!(await fs.pathExists(moduleDir))) {
         await prompts.log.warn('Module directory not found: ' + moduleDir);
         continue;
       }
tools/cli/installers/lib/ide/codex.js (1)

258-298: ⚠️ Potential issue | 🟡 Minor

Inconsistent silent gating within clearOldBmadFiles.

The directory-read error at line 268 is gated by !options.silent, but the per-entry error at line 295 always logs via prompts.log.message. During a silent installation (spinner running), the per-entry message could interleave with the spinner output.

🛡️ Proposed fix — gate per-entry message
       } catch (error) {
         // Skip files that can't be processed
-        await prompts.log.message(`  Skipping ${entry}: ${error.message}`);
+        if (!options.silent) await prompts.log.message(`  Skipping ${entry}: ${error.message}`);
       }
tools/cli/installers/lib/ide/manager.js (1)

170-213: ⚠️ Potential issue | 🟡 Minor

Inconsistent error return shape: reason vs error.

Line 176 returns { success: false, reason: 'unsupported' } but line 212 returns { success: false, ide: ideName, error: error.message }. The consumer in installer.js (lines 1049–1053) reads setupResult.error, so the unsupported-IDE case silently falls through to the 'failed' default. Consider unifying:

🛡️ Proposed fix
     if (!handler) {
       await prompts.log.warn(`IDE '${ideName}' is not yet supported`);
       await prompts.log.message(`Supported IDEs: ${[...this.handlers.keys()].join(', ')}`);
-      return { success: false, reason: 'unsupported' };
+      return { success: false, ide: ideName, error: 'unsupported IDE' };
     }
tools/cli/installers/lib/core/installer.js (1)

273-276: ⚠️ Potential issue | 🔴 Critical

bmadDir is not in scope at line 275 and will cause a ReferenceError.

bmadDir is declared at line 381 inside the try block, but line 275 references it in the else block that precedes the try. If this code path executes during a non-quick-update modify flow where a custom module's sourcePath starts with '_config', it will throw a ReferenceError.

Fix by declaring bmadDir before the else block:

const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
🤖 Fix all issues with AI agents
In `@tools/cli/installers/lib/core/config-collector.js`:
- Line 347: Remove the stray spacing console.log() call (the bare expression
console.log()) in config-collector.js and either delete it entirely or replace
it with the project's prompts-based spacing/output convention (e.g., use the
prompts library's separator/notice helper used elsewhere in this module). Locate
the exact console.log() expression and update it to match surrounding prompt
usage so output remains consistent with other prompt-driven messages.

In `@tools/cli/installers/lib/core/installer.js`:
- Around line 1017-1062: The code temporarily replaces console.log when
!config.verbose before iterating validIdes and awaiting this.ideManager.setup,
which leaves console.log broken if an awaited call throws; remove the global
monkey-patch entirely and rely on the existing silent/verbose flags (pass
silent: ideHasConfig and verbose: config.verbose to this.ideManager.setup and
fix any handlers that directly call console.log), or at minimum wrap the
suppression/restoration around the loop in a try/finally so console.log =
originalLog is always executed even if awaits inside the validIdes loop throw;
update references around validIdes, ideConfigurations, spinner, and
this.ideManager.setup accordingly.

In `@tools/cli/installers/lib/modules/manager.js`:
- Around line 626-630: In update(), the call to compileModuleAgents(sourcePath,
targetPath, moduleName, bmadDir) omits the fifth installer parameter, so sidecar
file tracking never gets populated; modify the call inside update() to pass the
installer object (the same value passed from install(), e.g. options.installer
or the local installer variable) so compileModuleAgents(...) receives the
installer and installer.installedFiles is populated during updates (ensure the
identifier matches the installer used elsewhere in this file).
- Around line 366-371: The silent-mode spinner stub inside createSpinner must
match the real spinner API from prompts.spinner: add no-op cancel() and clear()
methods and define isSpinning and isCancelled getters (returning false) so
callers won't throw or get undefined; update the returned object in
createSpinner (the stub that currently has start/stop/error/message) to include
cancel, clear, and the two getters while keeping existing no-op implementations
for start/stop/error/message to preserve behavior.

In `@tools/cli/lib/agent/installer.js`:
- Line 167: The defaultValue for prompts.text currently uses the expression
q.default || '' which incorrectly replaces legitimate falsy defaults like 0 or
"0"; update the code that constructs the prompts.text options (the block that
sets defaultValue for each question, referencing q.default and prompts.text) to
use the nullish coalescing operator (q.default ?? '') so only null or undefined
are replaced, preserving numeric 0 and string "0" as valid defaults.
- Line 169: The assignment answers[q.var] = response || q.default || ''
incorrectly treats intentional empty-string responses as missing; change the
fallback logic in the installer code that sets answers[q.var] (where response
and q.default are used) to use nullish coalescing so that only null/undefined
trigger the fallback (i.e., use response ?? q.default ?? ''), preserving
deliberate empty-string input.
- Around line 173-177: The boolean prompt call using prompts.confirm should pass
the wrapper's expected option name: replace the incorrect option key
"initialValue" with "default" so the call to prompts.confirm({ message:
q.prompt, default: q.default }) forwards q.default correctly; update the call
around prompts.confirm in installer.js (the block that assigns answers[q.var] =
response) to use "default" instead of "initialValue".

In `@tools/cli/lib/cli-utils.js`:
- Around line 61-76: The border color mapping in displayBox silently maps only
'green','red','yellow' to color functions and defaults everything else to cyan;
change formatBorder resolution in the displayBox function to use a lookup or
dynamic property access on the color object (from prompts.getColor())—e.g.,
check if color[borderColor] is a function and use it, otherwise fall back to
color.cyan—to support arbitrary valid color names (and keep existing behavior
for undefined borderStyle and options.title when calling prompts.box).

In `@tools/cli/lib/prompts.js`:
- Line 115: The message passthrough currently calls s.message(msg)
unconditionally (line with "message: (msg) => s.message(msg)"), which can run
when the spinner isn't active; guard this call with the same spinning flag used
by start/stop/error/cancel/clear so it only forwards to s.message when spinning
is true (otherwise no-op or return safely). Update the "message" property to
check the spinning variable before invoking s.message to avoid calling the
underlying `@clack/prompts` spinner when it's not active.
- Around line 648-673: The stream methods (stream.info, stream.success,
stream.step, stream.warn, stream.error, stream.message) call clack.stream.*
without awaiting or returning the resulting promise; update each async method in
the stream object to either return or await the inner call (e.g., return
clack.stream.info(generator) or await clack.stream.info(generator)) so callers
awaiting prompts.stream.info(...) properly wait for the underlying
clack.stream.* promise resolved; use getClack() as currently done and apply the
same change to all listed methods including message(generator, options).

In `@tools/cli/lib/ui.js`:
- Around line 964-966: Remove the unused variable availableNames: delete the
const availableNames = externalModuleChoices.map((c) => c.name).join(', '); line
from the module selection code so only the used message variable remains; ensure
no other code references availableNames (look for externalModuleChoices and
message in the same block) and run linter to confirm no unused-vars remain.
🧹 Nitpick comments (20)
tools/cli/installers/lib/custom/handler.js (1)

88-93: Logging migration looks correct, but error context is lost.

The await on prompts.log.warn() is correct. However, the old code likely included chalk-formatted path info. The new string concatenation approach works, but consider using template literals for consistency with the rest of the codebase (e.g., Line 352 uses template literals for a similar pattern).

Use template literal for consistency
-        await prompts.log.warn('YAML parse error in ' + configPath + ': ' + parseError.message);
+        await prompts.log.warn(`YAML parse error in ${configPath}: ${parseError.message}`);
tools/cli/installers/lib/modules/manager.js (1)

1282-1288: Default logger still uses raw console.* methods, inconsistent with migration.

The fallback logger at Lines 1284–1288 uses console.log, console.error, and console.warn. While this is a fallback for when no logger is provided, it means any module installer that doesn't receive a logger will bypass the prompts-based UI entirely. Consider providing a prompts-based default logger, or at minimum documenting why raw console is acceptable here.

tools/cli/lib/cli-utils.js (2)

22-45: getColor() is called on every invocation of displayLogo, displayBox, displayComplete, and displayError.

Each display method independently awaits prompts.getColor(). While getPicocolors caches the import, each call still traverses the async wrapper chain. If these methods are called in quick succession (e.g., during install flow), this is wasteful. Consider caching the color object at module scope after first resolution.

Example: cache color at module level
+let _color = null;
+async function getColor() {
+  if (!_color) _color = await prompts.getColor();
+  return _color;
+}
+
 const CLIUtils = {
   // ...
   async displayLogo(_clearScreen = true) {
     const version = this.getVersion();
-    const color = await prompts.getColor();
+    const color = await getColor();

148-153: clearLines uses raw process.stdout methods that may not exist in all environments.

process.stdout.moveCursor and process.stdout.clearLine are only available when stdout is a TTY. In piped/redirected output or CI environments, these will throw. This is pre-existing, but now that the rest of the module migrates to prompts-based output (which handles non-TTY gracefully), this method stands out as inconsistent.

Add TTY guard
   clearLines(lines) {
+    if (!process.stdout.isTTY) return;
     for (let i = 0; i < lines; i++) {
       process.stdout.moveCursor(0, -1);
       process.stdout.clearLine(1);
     }
   },
tools/cli/lib/prompts.js (2)

362-369: Coupling to private _isActionKey method of @clack/core.

Line 363 accesses prompt._isActionKey, which is a private implementation detail of AutocompletePrompt. This will break silently or throw if @clack/core renames or removes this method in a future version. The comment acknowledges this is a fix, but it should be defensive.

Add a guard
-  const originalIsActionKey = prompt._isActionKey.bind(prompt);
-  prompt._isActionKey = function (char, key) {
-    if (key && key.name === 'space') {
-      return true;
-    }
-    return originalIsActionKey(char, key);
-  };
+  if (typeof prompt._isActionKey === 'function') {
+    const originalIsActionKey = prompt._isActionKey.bind(prompt);
+    prompt._isActionKey = function (char, key) {
+      if (key && key.name === 'space') {
+        return true;
+      }
+      return originalIsActionKey(char, key);
+    };
+  }

531-556: log methods also don't await/return inner calls—but these are likely synchronous.

For consistency, and to future-proof against @clack/prompts making these async, consider return-ing the inner call, same as the stream methods issue. Currently callers await prompts.log.info(msg) which resolves to undefined from the await getClack() rather than from the actual log call.

tools/cli/installers/lib/message-loader.js (1)

80-80: Class field declaration placed after all method definitions.

The messages = null field initializer works, but placing it after all methods is unconventional and easy to miss. Moving it to the top of the class (or into the constructor) would improve discoverability.

tools/cli/commands/status.js (2)

32-32: Calling private method manifest._readRaw() from a command module.

_readRaw is conventionally private (underscore prefix). This couples status.js to an internal implementation detail of Manifest. If the manifest API already exposes a public method for reading data, prefer that; otherwise, promote _readRaw to a public method (e.g., readRaw or read).


22-22: Inline require('fs-extra') inside action handler.

fs-extra is already a top-level dependency in other files across this codebase. Moving it to the top of the file is cleaner and avoids repeated resolution on every invocation.

tools/cli/installers/lib/core/config-collector.js (1)

594-597: Empty if branch for core module.

The if (moduleName === 'core') block contains only a comment. This is a no-op that adds noise. Invert the condition to remove the empty branch.

Proposed fix
-        if (moduleName === 'core') {
-          // Core module: no confirm prompt, continues directly
-        } else {
+        if (moduleName !== 'core') {
           // Non-core modules: show "Accept Defaults?" confirm prompt
tools/cli/installers/lib/ide/kilo.js (1)

160-161: Inconsistent file-system access: inline require('fs-extra') vs. base-class helpers.

clearBmadWorkflows (line 160) and cleanup (line 175) each do an inline require('fs-extra'), while the rest of the class uses this.pathExists, this.readFile, this.writeFile, and this.ensureDir from BaseIdeSetup. This inconsistency makes it harder to mock/test and breaks the abstraction the base class provides.

Prefer using the base-class helpers throughout, or import fs-extra once at the top of the file.

Also applies to: 175-175

tools/cli/installers/lib/ide/kiro-cli.js (1)

17-21: JSDoc missing @param for options.

The cleanup signature was updated to accept options = {}, but the JSDoc block (lines 17–20) doesn't document it. Other methods like setup (line 40) do document @param {Object} options.

📝 Proposed doc fix
   /**
    * Cleanup old BMAD installation before reinstalling
    * `@param` {string} projectDir - Project directory
+   * `@param` {Object} [options] - Cleanup options
+   * `@param` {boolean} [options.silent] - Suppress log output
    */
   async cleanup(projectDir, options = {}) {
tools/cli/installers/lib/core/dependency-resolver.js (1)

553-554: Dead variable: content is read but never used.

Line 553 reads the file into content, but the actual parsing happens inside this.parseDependencies(...) at line 555, which re-reads the file independently. The content variable is unused.

🧹 Remove dead read
       if ((depPath.endsWith('.md') || depPath.endsWith('.yaml') || depPath.endsWith('.yml')) && (await fs.pathExists(depPath))) {
-        const content = await fs.readFile(depPath, 'utf8');
         const subDeps = await this.parseDependencies([
tools/cli/installers/lib/core/installer.js (3)

438-442: Shadowed prompts variable — redundant re-import.

Line 439 re-declares const prompts = require(...) inside a block scope, shadowing the module-level prompts imported at line 17. Since Node caches modules, this works identically, but it's dead weight and a readability hazard. This looks like a leftover from before the module-level import was added.

🧹 Remove redundant import
          if (modulesToRemove.length > 0) {
-           const prompts = require('../../../lib/prompts');
            if (spinner.isSpinning) {
              spinner.stop('Reviewing module changes');
            }

376-378: projectDir is re-declared inside try, shadowing the identical definition at Line 239.

Line 239: const projectDir = path.resolve(config.directory);
Line 378: const projectDir = path.resolve(config.directory); (inside try)

Both resolve to the same value. This shadowing means code before the try (lines 240–372) and code inside try reference different const bindings that happen to hold the same value. It's confusing and creates a maintenance trap — if one changes, the other won't.

♻️ Remove duplicate declaration
       // Resolve target directory (path.resolve handles platform differences)
-      const projectDir = path.resolve(config.directory);
+      // projectDir already declared at line 239

1070-1075: moduleLogger still uses raw console.log/console.error/console.warn — inconsistent with prompts migration.

The module logger at lines 1071–1075 creates a wrapper that calls console.log, console.error, and console.warn directly. Given that lines 1018-1021 suppress console.log, and the broader migration replaces console calls with prompts.log.*, this logger won't output anything in non-verbose mode (because console.log is overridden to a no-op). If the console.log suppression is removed (as recommended), this logger will start emitting raw console output that may interleave with the spinner.

Consider routing this through prompts as well, or noting that silent: true on line 1086/1101 should make the logger moot.

tools/cli/installers/lib/ide/codex.js (1)

286-293: Both isFile() and isDirectory() branches perform identical fs.remove — could be simplified.

Pre-existing, but since you're touching this method: the conditional is redundant because fs.remove handles both files and directories.

♻️ Simplify
       try {
-        const stat = await fs.stat(entryPath);
-        if (stat.isFile()) {
-          await fs.remove(entryPath);
-        } else if (stat.isDirectory()) {
-          await fs.remove(entryPath);
-        }
+        await fs.remove(entryPath);
       } catch (error) {
tools/cli/installers/lib/ide/manager.js (1)

183-208: Triplicated detail-building logic — consider extracting a helper.

The identical "collect non-zero counts into parts and join" pattern is repeated for three handler return shapes. A small helper would reduce duplication and make adding new handler types easier.

♻️ Proposed extraction
+  /**
+   * Build a human-readable detail string from count fields
+   */
+  _buildDetailString(counts) {
+    const labels = { agents: 'agents', modes: 'agents', workflows: 'workflows', tasks: 'tasks', tools: 'tools' };
+    const parts = [];
+    for (const [key, label] of Object.entries(labels)) {
+      if (counts[key] > 0) parts.push(`${counts[key]} ${label}`);
+    }
+    return parts.join(', ');
+  }

   // Then in setup():
-  if (handlerResult && handlerResult.results) {
-    const r = handlerResult.results;
-    const parts = [];
-    if (r.agents > 0) parts.push(`${r.agents} agents`);
-    // ... etc
-    detail = parts.join(', ');
-  } else if (handlerResult && handlerResult.counts) {
-    // ... etc
-  } else if (handlerResult && handlerResult.modes !== undefined) {
-    // ... etc
-  }
+  const rawCounts = handlerResult?.results || handlerResult?.counts || handlerResult || {};
+  detail = this._buildDetailString(rawCounts);
tools/cli/lib/agent/installer.js (2)

147-149: presetAnswers values are never validated or type-checked.

presetAnswers is spread directly into answers and can silently override defaults with wrong types (e.g., string where boolean is expected). There's no guard or schema validation. Consumers of answers downstream may break on unexpected types.


461-461: Redundant require('yaml') — already imported at the top of the file (Line 8).

yaml is required at the module level on Line 8 as yaml, but saveAgentSource (Line 461), createIdeSlashCommands (Line 542), and updateManifestYaml (Line 571) all do a local const yamlLib = require('yaml'). This is harmless (Node caches modules) but unnecessarily noisy — just use the existing yaml binding.

Address 31 issues across 14 CLI files found during PR bmad-code-org#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]>
@dracic
Copy link
Contributor Author

dracic commented Feb 7, 2026

  • compileModuleAgents lacks access to installer.copyFileWithPlaceholderReplacement. Architecturally complex refactor for a Tier 4 fix with low practical impact. Deferred.
  • update() JSDoc not updated to document new options parameter. Avoided adding docs to code we didn't author per project convention of minimal-touch changes.
  • dependency-resolver.js silently skips modules that aren't core/bmm without a warning log. External/custom modules are resolved through a different path, so a warning here would be misleading.
  • workflowsInstalled counter only counts .md files, not .yaml workflows. Matches the original behavior — counter was only moved, not redesigned - .yaml workflow files are tracked separately in copyWorkflowYamlStripped.
  • spinner.message() guard silently drops messages when spinner is stopped during prompting. This matches the behavior of start/stop/error which also no-op in invalid states. Queueing messages would add complexity with no user-facing benefit.
  • throw TypeError for async validation in password prompts. Intentional defensive guard — @clack/prompts doesn't support async validation and this fails fast with a clear message rather than silently misbehaving.
  • copyFileWithPlaceholderReplacement name is misleading in both installer.js and manager.js since placeholder replacement logic was removed. BMAD_FOLDER_NAME is now a constant (_bmad), so no replacement is needed. Renaming would touch 10+ call sites across the codebase for a cosmetic change.

@bmadcode
Copy link
Collaborator

bmadcode commented Feb 8, 2026

amazing thank you buddy @dracic !

@bmadcode bmadcode merged commit b1bfce9 into bmad-code-org:main Feb 8, 2026
5 checks passed
@dracic dracic deleted the feat/installer-refinement branch February 8, 2026 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants