2025-09-28 23:17:07 -05:00
const chalk = require ( 'chalk' ) ;
2025-12-23 07:01:36 -08:00
const inquirer = require ( 'inquirer' ) ;
2025-09-28 23:17:07 -05:00
const path = require ( 'node:path' ) ;
const os = require ( 'node:os' ) ;
const fs = require ( 'fs-extra' ) ;
const { CLIUtils } = require ( './cli-utils' ) ;
2025-12-08 12:24:30 -07:00
const { CustomHandler } = require ( '../installers/lib/custom/handler' ) ;
2025-09-28 23:17:07 -05:00
/ * *
* UI utilities for the installer
* /
class UI {
/ * *
* Prompt for installation configuration
* @ returns { Object } Installation configuration
* /
async promptInstall ( ) {
CLIUtils . displayLogo ( ) ;
2025-12-23 21:43:29 +08:00
// Display version-specific start message from install-messages.yaml
const { MessageLoader } = require ( '../installers/lib/message-loader' ) ;
const messageLoader = new MessageLoader ( ) ;
messageLoader . displayStartMessage ( ) ;
2025-12-16 18:22:46 +08:00
2025-09-28 23:17:07 -05:00
const confirmedDirectory = await this . getConfirmedDirectory ( ) ;
2025-10-02 21:45:59 -05:00
2025-11-08 15:19:19 -06:00
// Preflight: Check for legacy BMAD v4 footprints immediately after getting directory
const { Detector } = require ( '../installers/lib/core/detector' ) ;
const { Installer } = require ( '../installers/lib/core/installer' ) ;
const detector = new Detector ( ) ;
const installer = new Installer ( ) ;
const legacyV4 = await detector . detectLegacyV4 ( confirmedDirectory ) ;
if ( legacyV4 . hasLegacyV4 ) {
await installer . handleLegacyV4Migration ( confirmedDirectory , legacyV4 ) ;
}
2025-12-13 23:45:47 +08:00
// Check for legacy folders and prompt for rename before showing any menus
let hasLegacyCfg = false ;
let hasLegacyBmadFolder = false ;
let bmadDir = null ;
let legacyBmadPath = null ;
// First check for legacy .bmad folder (instead of _bmad)
2025-12-14 10:03:25 +08:00
// Only check if directory exists
if ( await fs . pathExists ( confirmedDirectory ) ) {
const entries = await fs . readdir ( confirmedDirectory , { withFileTypes : true } ) ;
for ( const entry of entries ) {
2025-12-23 20:04:42 +08:00
if ( entry . isDirectory ( ) && ( entry . name === '.bmad' || entry . name === 'bmad' ) ) {
2025-12-14 10:03:25 +08:00
hasLegacyBmadFolder = true ;
legacyBmadPath = path . join ( confirmedDirectory , '.bmad' ) ;
bmadDir = legacyBmadPath ;
// Check if it has _cfg folder
const cfgPath = path . join ( legacyBmadPath , '_cfg' ) ;
if ( await fs . pathExists ( cfgPath ) ) {
hasLegacyCfg = true ;
}
break ;
2025-12-13 23:45:47 +08:00
}
}
}
2025-12-23 20:04:42 +08:00
// If no .bmad or bmad found, check for current installations _bmad
2025-12-13 23:45:47 +08:00
if ( ! hasLegacyBmadFolder ) {
const bmadResult = await installer . findBmadDir ( confirmedDirectory ) ;
bmadDir = bmadResult . bmadDir ;
hasLegacyCfg = bmadResult . hasLegacyCfg ;
}
2025-12-23 20:04:42 +08:00
// Handle legacy .bmad or _cfg folder - these are very old (more than 2 versions behind)
// Show version warning instead of offering conversion
2025-12-13 23:45:47 +08:00
if ( hasLegacyBmadFolder || hasLegacyCfg ) {
2025-12-23 20:04:42 +08:00
console . log ( '' ) ;
console . log ( chalk . yellow . bold ( '⚠️ LEGACY INSTALLATION DETECTED' ) ) ;
console . log ( chalk . yellow ( '─' . repeat ( 80 ) ) ) ;
console . log (
chalk . yellow (
'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder - this is from a old BMAD version that is out of date for automatic upgrade, manual intervention required.' ,
) ,
) ;
console . log ( chalk . yellow ( 'This version is more than 2 alpha versions behind current.' ) ) ;
console . log ( '' ) ;
console . log ( chalk . dim ( 'For stability, we only support updates from the previous 2 alpha versions.' ) ) ;
console . log ( chalk . dim ( 'Legacy installations may have compatibility issues.' ) ) ;
console . log ( '' ) ;
console . log ( chalk . dim ( 'For the best experience, we strongly recommend:' ) ) ;
console . log ( chalk . dim ( ' 1. Delete your current BMAD installation folder (.bmad or bmad)' ) ) ;
console . log (
chalk . dim (
' 2. Run a fresh installation\n\nIf you do not want to start fresh, you can attempt to proceed beyond this point IF you have ensured the bmad folder is named _bmad, and under it there is a _config folder. If you have a folder under your bmad folder named _cfg, you would need to rename it _config, and then restart the installer.' ,
) ,
) ;
console . log ( '' ) ;
console . log ( chalk . dim ( 'Benefits of a fresh install:' ) ) ;
console . log ( chalk . dim ( ' • Cleaner configuration without legacy artifacts' ) ) ;
console . log ( chalk . dim ( ' • All new features properly configured' ) ) ;
console . log ( chalk . dim ( ' • Fewer potential conflicts' ) ) ;
console . log ( chalk . dim ( '' ) ) ;
console . log (
chalk . dim (
'If you have already produced output from an earlier alpha version, you can still retain those artifacts. After installation, ensure you configured during install the proper file locations for artifacts depending on the module you are using, or move the files to the proper locations.' ,
) ,
) ;
console . log ( chalk . yellow ( '─' . repeat ( 80 ) ) ) ;
console . log ( '' ) ;
2025-12-13 23:45:47 +08:00
2025-12-23 20:04:42 +08:00
const { proceed } = await inquirer . prompt ( [
2025-12-13 23:45:47 +08:00
{
2025-12-23 20:04:42 +08:00
type : 'list' ,
name : 'proceed' ,
message : 'What would you like to do?' ,
choices : [
{
name : 'Cancel and do a fresh install (recommended)' ,
value : 'cancel' ,
short : 'Cancel installation' ,
} ,
{
name : 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)' ,
value : 'proceed' ,
short : 'Proceed with update' ,
} ,
] ,
default : 'cancel' ,
2025-12-13 23:45:47 +08:00
} ,
] ) ;
2025-12-23 20:04:42 +08:00
if ( proceed === 'cancel' ) {
console . log ( '' ) ;
console . log ( chalk . cyan ( 'To do a fresh install:' ) ) ;
console . log ( chalk . dim ( ' 1. Delete the existing bmad folder in your project' ) ) ;
console . log ( chalk . dim ( " 2. Run 'bmad install' again" ) ) ;
console . log ( '' ) ;
2025-12-13 23:45:47 +08:00
process . exit ( 0 ) ;
return ;
}
const ora = require ( 'ora' ) ;
const spinner = ora ( 'Updating folder structure...' ) . start ( ) ;
try {
2025-12-23 20:04:42 +08:00
// Handle .bmad folder
2025-12-13 23:45:47 +08:00
if ( hasLegacyBmadFolder ) {
const newBmadPath = path . join ( confirmedDirectory , '_bmad' ) ;
await fs . move ( legacyBmadPath , newBmadPath ) ;
bmadDir = newBmadPath ;
spinner . succeed ( 'Renamed ".bmad" to "_bmad"' ) ;
}
2025-12-23 20:04:42 +08:00
// Handle _cfg folder (either from .bmad or standalone)
const cfgPath = path . join ( bmadDir , '_cfg' ) ;
if ( await fs . pathExists ( cfgPath ) ) {
2025-12-13 23:45:47 +08:00
spinner . start ( 'Renaming configuration folder...' ) ;
const newCfgPath = path . join ( bmadDir , '_config' ) ;
2025-12-23 20:04:42 +08:00
await fs . move ( cfgPath , newCfgPath ) ;
2025-12-13 23:45:47 +08:00
spinner . succeed ( 'Renamed "_cfg" to "_config"' ) ;
}
} catch ( error ) {
spinner . fail ( 'Failed to update folder structure' ) ;
console . error ( chalk . red ( ` Error: ${ error . message } ` ) ) ;
process . exit ( 1 ) ;
}
}
// Check if there's an existing BMAD installation (after any folder renames)
2025-10-02 21:45:59 -05:00
const hasExistingInstall = await fs . pathExists ( bmadDir ) ;
2025-12-15 10:59:15 +08:00
// Collect IDE tool selection early - we need this to know if we should ask about TTS
let toolSelection ;
let agentVibesConfig = { enabled : false , alreadyInstalled : false } ;
let claudeCodeSelected = false ;
if ( ! hasExistingInstall ) {
// For new installations, collect IDE selection first
// We don't have modules yet, so pass empty array
toolSelection = await this . promptToolSelection ( confirmedDirectory , [ ] ) ;
// Check if Claude Code was selected
claudeCodeSelected = toolSelection . ides && toolSelection . ides . includes ( 'claude-code' ) ;
// If Claude Code was selected, ask about TTS
if ( claudeCodeSelected ) {
const { enableTts } = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'enableTts' ,
message : 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?' ,
default : false ,
} ,
] ) ;
if ( enableTts ) {
agentVibesConfig = { enabled : true , alreadyInstalled : false } ;
}
}
}
2025-12-07 15:38:49 -06:00
let customContentConfig = { hasCustomContent : false } ;
2025-12-16 13:09:20 +08:00
if ( ! hasExistingInstall ) {
2025-12-07 20:46:09 -06:00
customContentConfig . _shouldAsk = true ;
2025-12-07 15:38:49 -06:00
}
2025-10-28 21:44:04 -05:00
// Track action type (only set if there's an existing installation)
let actionType ;
2025-10-02 21:45:59 -05:00
// Only show action menu if there's an existing installation
if ( hasExistingInstall ) {
2025-12-15 16:25:01 +08:00
// Get version information
2025-12-23 20:04:42 +08:00
const { existingInstall , bmadDir } = await this . getExistingInstallation ( confirmedDirectory ) ;
2025-12-15 16:25:01 +08:00
const packageJsonPath = path . join ( _ _dirname , '../../../package.json' ) ;
const currentVersion = require ( packageJsonPath ) . version ;
const installedVersion = existingInstall . version || 'unknown' ;
2025-12-23 20:04:42 +08:00
// Check if version is too old and warn user
const shouldProceed = await this . showOldAlphaVersionWarning ( installedVersion , currentVersion , path . basename ( bmadDir ) ) ;
// If user chose to cancel, exit the installer
if ( ! shouldProceed ) {
process . exit ( 0 ) ;
return ;
}
2025-12-15 16:25:01 +08:00
// Build menu choices dynamically
const choices = [ ] ;
// Always show Quick Update first (allows refreshing installation even on same version)
if ( installedVersion !== 'unknown' ) {
choices . push ( {
name : ` Quick Update (v ${ installedVersion } → v ${ currentVersion } ) ` ,
value : 'quick-update' ,
} ) ;
}
2025-12-17 17:58:37 +08:00
// Add custom agent compilation option
if ( installedVersion !== 'unknown' ) {
choices . push ( {
name : 'Recompile Agents (apply customizations only)' ,
value : 'compile-agents' ,
} ) ;
}
2025-12-15 16:25:01 +08:00
// Common actions
2025-12-16 01:25:49 +08:00
choices . push ( { name : 'Modify BMAD Installation' , value : 'update' } ) ;
2025-12-15 16:25:01 +08:00
2025-10-28 21:44:04 -05:00
const promptResult = await inquirer . prompt ( [
2025-10-02 21:45:59 -05:00
{
type : 'list' ,
name : 'actionType' ,
message : 'What would you like to do?' ,
2025-12-15 16:25:01 +08:00
choices : choices ,
default : choices [ 0 ] . value , // Use the first option as default
2025-10-02 21:45:59 -05:00
} ,
] ) ;
2025-10-28 21:44:04 -05:00
// Extract actionType from prompt result
actionType = promptResult . actionType ;
2025-10-26 16:17:37 -05:00
// Handle quick update separately
if ( actionType === 'quick-update' ) {
2025-12-07 15:38:49 -06:00
// Quick update doesn't install custom content - just updates existing modules
2025-10-26 16:17:37 -05:00
return {
actionType : 'quick-update' ,
directory : confirmedDirectory ,
2025-12-07 15:38:49 -06:00
customContent : { hasCustomContent : false } ,
2025-10-26 16:17:37 -05:00
} ;
}
2025-12-17 17:58:37 +08:00
// Handle compile agents separately
if ( actionType === 'compile-agents' ) {
// Only recompile agents with customizations, don't update any files
return {
actionType : 'compile-agents' ,
directory : confirmedDirectory ,
customContent : { hasCustomContent : false } ,
} ;
}
2025-12-15 17:30:12 +08:00
// If actionType === 'update', handle it with the new flow
// Return early with modify configuration
if ( actionType === 'update' ) {
// Get existing installation info
const { installedModuleIds } = await this . getExistingInstallation ( confirmedDirectory ) ;
2025-10-28 12:47:45 -05:00
2025-12-15 17:30:12 +08:00
console . log ( chalk . dim ( ` Found existing modules: ${ [ ... installedModuleIds ] . join ( ', ' ) } ` ) ) ;
const { changeModuleSelection } = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'changeModuleSelection' ,
2025-12-16 01:25:49 +08:00
message : 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?' ,
2025-12-15 17:30:12 +08:00
default : false ,
} ,
] ) ;
2025-12-15 10:59:15 +08:00
2025-12-15 17:30:12 +08:00
let selectedModules = [ ] ;
if ( changeModuleSelection ) {
// Show module selection with existing modules pre-selected
const moduleChoices = await this . getModuleChoices ( new Set ( installedModuleIds ) , { hasCustomContent : false } ) ;
selectedModules = await this . selectModules ( moduleChoices , [ ... installedModuleIds ] ) ;
} else {
selectedModules = [ ... installedModuleIds ] ;
}
2025-12-15 10:59:15 +08:00
2025-12-15 23:53:26 +08:00
// After module selection, ask about custom modules
2025-12-16 20:33:10 +08:00
console . log ( '' ) ;
const { changeCustomModules } = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'changeCustomModules' ,
message : 'Modify custom module selection (add, update, or remove custom modules/agents/workflows)?' ,
default : false ,
} ,
] ) ;
let customModuleResult = { selectedCustomModules : [ ] , customContentConfig : { hasCustomContent : false } } ;
if ( changeCustomModules ) {
customModuleResult = await this . handleCustomModulesInModifyFlow ( confirmedDirectory , selectedModules ) ;
} else {
// Preserve existing custom modules if user doesn't want to modify them
const { Installer } = require ( '../installers/lib/core/installer' ) ;
const installer = new Installer ( ) ;
const { bmadDir } = await installer . findBmadDir ( confirmedDirectory ) ;
const cacheDir = path . join ( bmadDir , '_config' , 'custom' ) ;
if ( await fs . pathExists ( cacheDir ) ) {
const entries = await fs . readdir ( cacheDir , { withFileTypes : true } ) ;
for ( const entry of entries ) {
if ( entry . isDirectory ( ) ) {
customModuleResult . selectedCustomModules . push ( entry . name ) ;
}
}
}
}
2025-12-15 23:53:26 +08:00
// Merge any selected custom modules
if ( customModuleResult . selectedCustomModules . length > 0 ) {
selectedModules . push ( ... customModuleResult . selectedCustomModules ) ;
}
2025-12-15 17:30:12 +08:00
// Get tool selection
const toolSelection = await this . promptToolSelection ( confirmedDirectory , selectedModules ) ;
// TTS configuration - ask right after tool selection (matches new install flow)
const hasClaudeCode = toolSelection . ides && toolSelection . ides . includes ( 'claude-code' ) ;
let enableTts = false ;
if ( hasClaudeCode ) {
const { enableTts : enable } = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'enableTts' ,
message : 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?' ,
default : false ,
} ,
] ) ;
enableTts = enable ;
}
2025-12-15 11:33:01 +08:00
2025-12-15 17:30:12 +08:00
// Core config with existing defaults (ask after TTS)
const coreConfig = await this . collectCoreConfig ( confirmedDirectory ) ;
2025-12-15 11:33:01 +08:00
2025-12-15 17:30:12 +08:00
return {
actionType : 'update' ,
directory : confirmedDirectory ,
installCore : true ,
modules : selectedModules ,
ides : toolSelection . ides ,
skipIde : toolSelection . skipIde ,
coreConfig : coreConfig ,
2025-12-15 23:53:26 +08:00
customContent : customModuleResult . customContentConfig ,
2025-12-15 17:30:12 +08:00
enableAgentVibes : enableTts ,
agentVibesInstalled : false ,
} ;
}
2025-12-14 10:03:25 +08:00
}
2025-12-15 17:30:12 +08:00
// This section is only for new installations (update returns early above)
2025-09-28 23:17:07 -05:00
const { installedModuleIds } = await this . getExistingInstallation ( confirmedDirectory ) ;
2025-12-15 10:59:15 +08:00
2025-12-15 17:30:12 +08:00
// Ask about official modules for new installations
const { wantsOfficialModules } = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'wantsOfficialModules' ,
2025-12-16 01:25:49 +08:00
message : 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?' ,
2025-12-15 17:30:12 +08:00
default : true ,
} ,
] ) ;
2025-12-06 16:56:09 -06:00
2025-12-15 17:30:12 +08:00
let selectedOfficialModules = [ ] ;
if ( wantsOfficialModules ) {
const moduleChoices = await this . getModuleChoices ( installedModuleIds , { hasCustomContent : false } ) ;
selectedOfficialModules = await this . selectModules ( moduleChoices ) ;
2025-12-07 20:46:09 -06:00
}
2025-12-15 17:30:12 +08:00
// Ask about custom content
const { wantsCustomContent } = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'wantsCustomContent' ,
2025-12-16 01:25:49 +08:00
message : 'Would you like to install a local custom module (this includes custom agents and workflows also)?' ,
2025-12-15 17:30:12 +08:00
default : false ,
} ,
] ) ;
2025-12-15 10:59:15 +08:00
2025-12-15 17:30:12 +08:00
if ( wantsCustomContent ) {
customContentConfig = await this . promptCustomContentSource ( ) ;
2025-12-06 16:56:09 -06:00
}
2025-09-28 23:17:07 -05:00
2025-12-15 17:30:12 +08:00
// Store the selected modules for later
customContentConfig . _selectedOfficialModules = selectedOfficialModules ;
// Build the final list of selected modules
let selectedModules = customContentConfig . _selectedOfficialModules || [ ] ;
// Add custom content modules if any were selected
if ( customContentConfig && customContentConfig . selectedModuleIds ) {
selectedModules = [ ... selectedModules , ... customContentConfig . selectedModuleIds ] ;
2025-12-15 10:59:15 +08:00
}
Add Text-to-Speech Integration via TTS_INJECTION System (#934)
* feat: Add provider-agnostic TTS integration via injection point system
Implements comprehensive Text-to-Speech integration for BMAD agents using a generic
TTS_INJECTION marker system. When AgentVibes (or any compatible TTS provider) is
installed, all BMAD agents can speak their responses with unique AI voices.
## Key Features
**Provider-Agnostic Architecture**
- Uses generic `TTS_INJECTION` markers instead of vendor-specific naming
- Future-proof for multiple TTS providers beyond AgentVibes
- Clean separation - BMAD stays TTS-agnostic, providers handle injection
**Installation Flow**
- BMAD → AgentVibes: TTS instructions injected when AgentVibes detects existing BMAD installation
- AgentVibes → BMAD: TTS instructions injected during BMAD installation when AgentVibes detected
- User must manually create voice assignment file when AgentVibes installs first (documented limitation)
**Party Mode Voice Support**
- Each agent speaks with unique assigned voice in multi-agent discussions
- PM, Architect, Developer, Analyst, UX Designer, etc. - all with distinct voices
**Zero Breaking Changes**
- Fully backward compatible - works without any TTS provider
- `TTS_INJECTION` markers are benign HTML comments if not processed
- No changes to existing agent behavior or non-TTS workflows
## Implementation Details
**Files Modified:**
- `tools/cli/installers/lib/core/installer.js` - TTS injection processing logic
- `tools/cli/lib/ui.js` - AgentVibes detection and installation prompts
- `tools/cli/commands/install.js` - Post-install guidance for AgentVibes setup
- `src/utility/models/fragments/activation-rules.xml` - TTS_INJECTION marker for agents
- `src/core/workflows/party-mode/instructions.md` - TTS_INJECTION marker for party mode
**Injection Point System:**
```xml
<rules>
- ALWAYS communicate in {communication_language}
<!-- TTS_INJECTION:agent-tts -->
- Stay in character until exit selected
</rules>
```
When AgentVibes is detected, the installer replaces this marker with:
```
- When responding to user messages, speak your responses using TTS:
Call: `.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'` after each response
IMPORTANT: Use single quotes - do NOT escape special characters like ! or $
```
**Special Character Handling:**
- Explicit guidance to use single quotes without escaping
- Prevents "backslash exclamation" artifacts in speech
**User Experience:**
```
User: "How should we architect this feature?"
Architect: [Text response] + 🔊 [Professional voice explains architecture]
```
Party Mode:
```
PM (John): "I'll focus on user value..." 🔊 [Male professional voice]
UX Designer (Sara): "From a user perspective..." 🔊 [Female voice]
Architect (Marcus): "The technical approach..." 🔊 [Male technical voice]
```
## Testing
**Unit Tests:** ✅ 62/62 passing
- 49/49 schema validation tests
- 13/13 installation component tests
**Integration Testing:**
- ✅ BMAD → AgentVibes (automatic injection)
- ✅ AgentVibes → BMAD (automatic injection)
- ✅ No TTS provider (markers remain as comments)
## Documentation
Comprehensive testing guide created with:
- Both installation scenario walkthroughs
- Verification commands and expected outputs
- Troubleshooting guidance
## Known Limitations
**AgentVibes → BMAD Installation Order:**
When AgentVibes installs first, voice assignment file must be created manually:
```bash
mkdir -p .bmad/_cfg
cat > .bmad/_cfg/agent-voice-map.csv << 'EOF'
agent_id,voice_name
pm,en_US-ryan-high
architect,en_US-danny-low
dev,en_US-joe-medium
EOF
```
This limitation exists to prevent false legacy v4 detection warnings from BMAD installer.
**Recommended:** Install BMAD first, then AgentVibes for automatic voice assignment.
## Related Work
**Companion Implementation:**
- Repository: paulpreibisch/AgentVibes
- Commits: 6 commits implementing injection processing and voice routing
- Features: Retroactive injection, file path extraction, escape stripping
**GitHub Issues:**
- paulpreibisch/AgentVibes#36 - BMAD agent ID support
## Breaking Changes
None. Feature is opt-in and requires separate TTS provider installation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Enforce project hooks over global hooks in party mode
before, claude would sometimes favor global agent vibes hooks over project specific
* feat: Automate AgentVibes installer invocation after BMAD install
Instead of showing manual installation instructions, the installer now:
- Prompts "Press Enter to start AgentVibes installer..."
- Automatically runs npx agentvibes@latest install
- Handles errors gracefully with fallback instructions
This provides a seamless installation flow matching the test script's
interactive approach.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: Add automated testing script and guide for PR #934
Added comprehensive testing tools for AgentVibes party mode integration:
- test-bmad-pr.sh: Fully automated installation and verification script
- Interactive mode selection (official PR or custom fork)
- Automatic BMAD CLI setup and linking
- AgentVibes installation with guided prompts
- Built-in verification checks for voice maps and hooks
- Saved configuration for quick re-testing
- TESTING.md: Complete testing documentation
- Quick start with one-line npx command
- Manual installation alternative
- Troubleshooting guide
- Cleanup instructions
Testers can now run a single command to test the full AgentVibes integration
without needing to understand the complex setup process.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Add shell: true to npx execSync to prevent permission denied error
The execSync call for 'npx agentvibes@latest install' was failing with
'Permission denied' because the shell was trying to execute 'agentvibes@latest'
directly instead of passing it as an argument to npx.
Adding shell: true ensures the command runs in a proper shell context
where npx can correctly interpret the @latest version syntax.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Remove duplicate AgentVibes installation step from test script
The test script was calling AgentVibes installer twice:
1. BMAD installer now automatically runs AgentVibes (new feature)
2. Test script had a separate Step 6 that also ran AgentVibes
This caused the installer to run twice, with the second call failing
because it was already installed.
Changes:
- Removed redundant Step 6 (AgentVibes installation)
- Updated Step 5 to indicate it includes AgentVibes
- Updated step numbers from 7 to 6 throughout
- Added guidance that AgentVibes runs automatically
Now the flow is cleaner: BMAD installer handles everything!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Address bmadcode review - preserve variables and move TTS logic to injection
Fixes requested changes from PR review:
1. Preserve {bmad_folder} variable placeholder
- Changed: {project_root}/.bmad/core/tasks/workflow.xml
- To: {project_root}/{bmad_folder}/core/tasks/workflow.xml
- Allows users to choose custom BMAD folder names during installation
2. Move TTS-specific hook guidance to injection system
- Removed hardcoded hook enforcement from source files
- Added hook guidance to processTTSInjectionPoints() in installer.js
- Now only appears when AgentVibes is installed (via TTS_INJECTION)
3. Maintain TTS-agnostic source architecture
- Source files remain clean of TTS-specific instructions
- TTS details injected at install-time only when needed
- Preserves provider-agnostic design principle
Changes made:
- src/core/workflows/party-mode/instructions.md
- Reverted .bmad to {bmad_folder} variable
- Replaced hardcoded hook guidance with <!-- TTS_INJECTION:party-mode -->
- Removed <note> about play-tts.sh hook location
- tools/cli/installers/lib/core/installer.js
- Added hook enforcement to party-mode injection replacement
- Guidance now injected only when enableAgentVibes is true
Addresses review comments from bmadcode:
- "needs to remain the variable. it will get set in the file at the install destination."
- "items like this we will need to inject if user is using claude and TTS"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Change 'claude-code' to 'claude' in test script instructions
The correct command to start Claude is 'claude', not 'claude-code'.
Updated line 362-363 in test-bmad-pr.sh to show the correct command.
* fix: Remove npm link from test script to avoid global namespace pollution
- Removed 'npm link' command that was installing BMAD globally
- Changed 'bmad install' to direct node execution using local clone
- Updated success message to reflect no global installation
This keeps testing fully isolated and prevents conflicts with:
- Existing BMAD installations
- Future official BMAD installs
- Orphaned symlinks when test directory is deleted
The test script now runs completely self-contained without modifying
the user's global npm environment.
---------
Co-authored-by: Claude Code <claude@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Preibisch <paul@paulpreibisch.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2025-11-26 08:51:57 -07:00
2025-12-15 17:30:12 +08:00
// Remove core if it's in the list (it's always installed)
selectedModules = selectedModules . filter ( ( m ) => m !== 'core' ) ;
// Tool selection (already done for new installs at the beginning)
if ( ! toolSelection ) {
toolSelection = await this . promptToolSelection ( confirmedDirectory , selectedModules ) ;
2025-12-15 10:59:15 +08:00
}
2025-11-04 21:58:41 -06:00
2025-12-15 17:30:12 +08:00
// Collect configurations for new installations
const coreConfig = await this . collectCoreConfig ( confirmedDirectory ) ;
// TTS already handled at the beginning for new installs
2025-09-28 23:17:07 -05:00
return {
2025-12-15 17:30:12 +08:00
actionType : 'install' ,
2025-09-28 23:17:07 -05:00
directory : confirmedDirectory ,
2025-12-15 17:30:12 +08:00
installCore : true ,
2025-09-28 23:17:07 -05:00
modules : selectedModules ,
2025-10-28 12:47:45 -05:00
ides : toolSelection . ides ,
skipIde : toolSelection . skipIde ,
2025-12-15 17:30:12 +08:00
coreConfig : coreConfig ,
2025-12-07 13:39:03 -06:00
customContent : customContentConfig ,
enableAgentVibes : agentVibesConfig . enabled ,
Add Text-to-Speech Integration via TTS_INJECTION System (#934)
* feat: Add provider-agnostic TTS integration via injection point system
Implements comprehensive Text-to-Speech integration for BMAD agents using a generic
TTS_INJECTION marker system. When AgentVibes (or any compatible TTS provider) is
installed, all BMAD agents can speak their responses with unique AI voices.
## Key Features
**Provider-Agnostic Architecture**
- Uses generic `TTS_INJECTION` markers instead of vendor-specific naming
- Future-proof for multiple TTS providers beyond AgentVibes
- Clean separation - BMAD stays TTS-agnostic, providers handle injection
**Installation Flow**
- BMAD → AgentVibes: TTS instructions injected when AgentVibes detects existing BMAD installation
- AgentVibes → BMAD: TTS instructions injected during BMAD installation when AgentVibes detected
- User must manually create voice assignment file when AgentVibes installs first (documented limitation)
**Party Mode Voice Support**
- Each agent speaks with unique assigned voice in multi-agent discussions
- PM, Architect, Developer, Analyst, UX Designer, etc. - all with distinct voices
**Zero Breaking Changes**
- Fully backward compatible - works without any TTS provider
- `TTS_INJECTION` markers are benign HTML comments if not processed
- No changes to existing agent behavior or non-TTS workflows
## Implementation Details
**Files Modified:**
- `tools/cli/installers/lib/core/installer.js` - TTS injection processing logic
- `tools/cli/lib/ui.js` - AgentVibes detection and installation prompts
- `tools/cli/commands/install.js` - Post-install guidance for AgentVibes setup
- `src/utility/models/fragments/activation-rules.xml` - TTS_INJECTION marker for agents
- `src/core/workflows/party-mode/instructions.md` - TTS_INJECTION marker for party mode
**Injection Point System:**
```xml
<rules>
- ALWAYS communicate in {communication_language}
<!-- TTS_INJECTION:agent-tts -->
- Stay in character until exit selected
</rules>
```
When AgentVibes is detected, the installer replaces this marker with:
```
- When responding to user messages, speak your responses using TTS:
Call: `.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'` after each response
IMPORTANT: Use single quotes - do NOT escape special characters like ! or $
```
**Special Character Handling:**
- Explicit guidance to use single quotes without escaping
- Prevents "backslash exclamation" artifacts in speech
**User Experience:**
```
User: "How should we architect this feature?"
Architect: [Text response] + 🔊 [Professional voice explains architecture]
```
Party Mode:
```
PM (John): "I'll focus on user value..." 🔊 [Male professional voice]
UX Designer (Sara): "From a user perspective..." 🔊 [Female voice]
Architect (Marcus): "The technical approach..." 🔊 [Male technical voice]
```
## Testing
**Unit Tests:** ✅ 62/62 passing
- 49/49 schema validation tests
- 13/13 installation component tests
**Integration Testing:**
- ✅ BMAD → AgentVibes (automatic injection)
- ✅ AgentVibes → BMAD (automatic injection)
- ✅ No TTS provider (markers remain as comments)
## Documentation
Comprehensive testing guide created with:
- Both installation scenario walkthroughs
- Verification commands and expected outputs
- Troubleshooting guidance
## Known Limitations
**AgentVibes → BMAD Installation Order:**
When AgentVibes installs first, voice assignment file must be created manually:
```bash
mkdir -p .bmad/_cfg
cat > .bmad/_cfg/agent-voice-map.csv << 'EOF'
agent_id,voice_name
pm,en_US-ryan-high
architect,en_US-danny-low
dev,en_US-joe-medium
EOF
```
This limitation exists to prevent false legacy v4 detection warnings from BMAD installer.
**Recommended:** Install BMAD first, then AgentVibes for automatic voice assignment.
## Related Work
**Companion Implementation:**
- Repository: paulpreibisch/AgentVibes
- Commits: 6 commits implementing injection processing and voice routing
- Features: Retroactive injection, file path extraction, escape stripping
**GitHub Issues:**
- paulpreibisch/AgentVibes#36 - BMAD agent ID support
## Breaking Changes
None. Feature is opt-in and requires separate TTS provider installation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Enforce project hooks over global hooks in party mode
before, claude would sometimes favor global agent vibes hooks over project specific
* feat: Automate AgentVibes installer invocation after BMAD install
Instead of showing manual installation instructions, the installer now:
- Prompts "Press Enter to start AgentVibes installer..."
- Automatically runs npx agentvibes@latest install
- Handles errors gracefully with fallback instructions
This provides a seamless installation flow matching the test script's
interactive approach.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: Add automated testing script and guide for PR #934
Added comprehensive testing tools for AgentVibes party mode integration:
- test-bmad-pr.sh: Fully automated installation and verification script
- Interactive mode selection (official PR or custom fork)
- Automatic BMAD CLI setup and linking
- AgentVibes installation with guided prompts
- Built-in verification checks for voice maps and hooks
- Saved configuration for quick re-testing
- TESTING.md: Complete testing documentation
- Quick start with one-line npx command
- Manual installation alternative
- Troubleshooting guide
- Cleanup instructions
Testers can now run a single command to test the full AgentVibes integration
without needing to understand the complex setup process.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Add shell: true to npx execSync to prevent permission denied error
The execSync call for 'npx agentvibes@latest install' was failing with
'Permission denied' because the shell was trying to execute 'agentvibes@latest'
directly instead of passing it as an argument to npx.
Adding shell: true ensures the command runs in a proper shell context
where npx can correctly interpret the @latest version syntax.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Remove duplicate AgentVibes installation step from test script
The test script was calling AgentVibes installer twice:
1. BMAD installer now automatically runs AgentVibes (new feature)
2. Test script had a separate Step 6 that also ran AgentVibes
This caused the installer to run twice, with the second call failing
because it was already installed.
Changes:
- Removed redundant Step 6 (AgentVibes installation)
- Updated Step 5 to indicate it includes AgentVibes
- Updated step numbers from 7 to 6 throughout
- Added guidance that AgentVibes runs automatically
Now the flow is cleaner: BMAD installer handles everything!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Address bmadcode review - preserve variables and move TTS logic to injection
Fixes requested changes from PR review:
1. Preserve {bmad_folder} variable placeholder
- Changed: {project_root}/.bmad/core/tasks/workflow.xml
- To: {project_root}/{bmad_folder}/core/tasks/workflow.xml
- Allows users to choose custom BMAD folder names during installation
2. Move TTS-specific hook guidance to injection system
- Removed hardcoded hook enforcement from source files
- Added hook guidance to processTTSInjectionPoints() in installer.js
- Now only appears when AgentVibes is installed (via TTS_INJECTION)
3. Maintain TTS-agnostic source architecture
- Source files remain clean of TTS-specific instructions
- TTS details injected at install-time only when needed
- Preserves provider-agnostic design principle
Changes made:
- src/core/workflows/party-mode/instructions.md
- Reverted .bmad to {bmad_folder} variable
- Replaced hardcoded hook guidance with <!-- TTS_INJECTION:party-mode -->
- Removed <note> about play-tts.sh hook location
- tools/cli/installers/lib/core/installer.js
- Added hook enforcement to party-mode injection replacement
- Guidance now injected only when enableAgentVibes is true
Addresses review comments from bmadcode:
- "needs to remain the variable. it will get set in the file at the install destination."
- "items like this we will need to inject if user is using claude and TTS"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Change 'claude-code' to 'claude' in test script instructions
The correct command to start Claude is 'claude', not 'claude-code'.
Updated line 362-363 in test-bmad-pr.sh to show the correct command.
* fix: Remove npm link from test script to avoid global namespace pollution
- Removed 'npm link' command that was installing BMAD globally
- Changed 'bmad install' to direct node execution using local clone
- Updated success message to reflect no global installation
This keeps testing fully isolated and prevents conflicts with:
- Existing BMAD installations
- Future official BMAD installs
- Orphaned symlinks when test directory is deleted
The test script now runs completely self-contained without modifying
the user's global npm environment.
---------
Co-authored-by: Claude Code <claude@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Preibisch <paul@paulpreibisch.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2025-11-26 08:51:57 -07:00
agentVibesInstalled : agentVibesConfig . alreadyInstalled ,
2025-09-28 23:17:07 -05:00
} ;
}
/ * *
* Prompt for tool / IDE selection ( called after module configuration )
* @ param { string } projectDir - Project directory to check for existing IDEs
* @ param { Array } selectedModules - Selected modules from configuration
* @ returns { Object } Tool configuration
* /
async promptToolSelection ( projectDir , selectedModules ) {
2025-11-12 22:40:45 -06:00
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
2025-09-28 23:17:07 -05:00
const { Detector } = require ( '../installers/lib/core/detector' ) ;
2025-11-12 22:40:45 -06:00
const { Installer } = require ( '../installers/lib/core/installer' ) ;
2025-09-28 23:17:07 -05:00
const detector = new Detector ( ) ;
2025-11-12 22:40:45 -06:00
const installer = new Installer ( ) ;
2025-12-13 23:45:47 +08:00
const bmadResult = await installer . findBmadDir ( projectDir || process . cwd ( ) ) ;
const bmadDir = bmadResult . bmadDir ;
2025-09-28 23:17:07 -05:00
const existingInstall = await detector . detect ( bmadDir ) ;
const configuredIdes = existingInstall . ides || [ ] ;
// Get IDE manager to fetch available IDEs dynamically
const { IdeManager } = require ( '../installers/lib/ide/manager' ) ;
const ideManager = new IdeManager ( ) ;
const preferredIdes = ideManager . getPreferredIdes ( ) ;
const otherIdes = ideManager . getOtherIdes ( ) ;
// Build IDE choices array with separators
const ideChoices = [ ] ;
const processedIdes = new Set ( ) ;
// First, add previously configured IDEs at the top, marked with ✅
if ( configuredIdes . length > 0 ) {
ideChoices . push ( new inquirer . Separator ( '── Previously Configured ──' ) ) ;
for ( const ideValue of configuredIdes ) {
2025-10-26 19:38:38 -05:00
// Skip empty or invalid IDE values
if ( ! ideValue || typeof ideValue !== 'string' ) {
continue ;
}
2025-09-28 23:17:07 -05:00
// Find the IDE in either preferred or other lists
const preferredIde = preferredIdes . find ( ( ide ) => ide . value === ideValue ) ;
const otherIde = otherIdes . find ( ( ide ) => ide . value === ideValue ) ;
const ide = preferredIde || otherIde ;
if ( ide ) {
ideChoices . push ( {
name : ` ${ ide . name } ✅ ` ,
value : ide . value ,
checked : true , // Previously configured IDEs are checked by default
} ) ;
processedIdes . add ( ide . value ) ;
2025-10-26 19:38:38 -05:00
} else {
// Warn about unrecognized IDE (but don't fail)
console . log ( chalk . yellow ( ` ⚠️ Previously configured IDE ' ${ ideValue } ' is no longer available ` ) ) ;
2025-09-28 23:17:07 -05:00
}
}
}
// Add preferred tools (excluding already processed)
const remainingPreferred = preferredIdes . filter ( ( ide ) => ! processedIdes . has ( ide . value ) ) ;
if ( remainingPreferred . length > 0 ) {
ideChoices . push ( new inquirer . Separator ( '── Recommended Tools ──' ) ) ;
for ( const ide of remainingPreferred ) {
ideChoices . push ( {
name : ` ${ ide . name } ⭐ ` ,
value : ide . value ,
checked : false ,
} ) ;
processedIdes . add ( ide . value ) ;
}
}
// Add other tools (excluding already processed)
const remainingOther = otherIdes . filter ( ( ide ) => ! processedIdes . has ( ide . value ) ) ;
if ( remainingOther . length > 0 ) {
ideChoices . push ( new inquirer . Separator ( '── Additional Tools ──' ) ) ;
for ( const ide of remainingOther ) {
ideChoices . push ( {
name : ide . name ,
value : ide . value ,
checked : false ,
} ) ;
}
}
2025-11-19 22:12:45 -06:00
let answers ;
let userConfirmedNoTools = false ;
// Loop until user selects at least one tool OR explicitly confirms no tools
while ( ! userConfirmedNoTools ) {
answers = await inquirer . prompt ( [
{
type : 'checkbox' ,
name : 'ides' ,
message : 'Select tools to configure:' ,
choices : ideChoices ,
2025-12-15 11:33:01 +08:00
pageSize : 30 ,
2025-11-19 22:12:45 -06:00
} ,
] ) ;
// If tools were selected, we're done
if ( answers . ides && answers . ides . length > 0 ) {
break ;
}
// Warn that no tools were selected - users often miss the spacebar requirement
console . log ( ) ;
console . log ( chalk . red . bold ( '⚠️ WARNING: No tools were selected!' ) ) ;
console . log ( chalk . red ( ' You must press SPACEBAR to select items, then ENTER to confirm.' ) ) ;
console . log ( chalk . red ( ' Simply highlighting an item does NOT select it.' ) ) ;
console . log ( ) ;
const { goBack } = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'goBack' ,
message : chalk . yellow ( 'Would you like to go back and select at least one tool?' ) ,
default : true ,
} ,
] ) ;
if ( goBack ) {
2025-12-15 11:05:27 +08:00
// Re-display a message before looping back
2025-11-19 22:12:45 -06:00
console . log ( ) ;
} else {
// User explicitly chose to proceed without tools
userConfirmedNoTools = true ;
}
}
2025-09-28 23:17:07 -05:00
return {
ides : answers . ides || [ ] ,
skipIde : ! answers . ides || answers . ides . length === 0 ,
} ;
}
/ * *
* Prompt for update configuration
* @ returns { Object } Update configuration
* /
async promptUpdate ( ) {
const answers = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'backupFirst' ,
message : 'Create backup before updating?' ,
default : true ,
} ,
{
type : 'confirm' ,
name : 'preserveCustomizations' ,
message : 'Preserve local customizations?' ,
default : true ,
} ,
] ) ;
return answers ;
}
/ * *
* Prompt for module selection
* @ param { Array } modules - Available modules
* @ returns { Array } Selected modules
* /
async promptModules ( modules ) {
const choices = modules . map ( ( mod ) => ( {
name : ` ${ mod . name } - ${ mod . description } ` ,
value : mod . id ,
checked : false ,
} ) ) ;
const { selectedModules } = await inquirer . prompt ( [
{
type : 'checkbox' ,
name : 'selectedModules' ,
message : 'Select modules to add:' ,
choices ,
validate : ( answer ) => {
if ( answer . length === 0 ) {
return 'You must choose at least one module.' ;
}
return true ;
} ,
} ,
] ) ;
return selectedModules ;
}
/ * *
* Confirm action
* @ param { string } message - Confirmation message
* @ param { boolean } defaultValue - Default value
* @ returns { boolean } User confirmation
* /
async confirm ( message , defaultValue = false ) {
const { confirmed } = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'confirmed' ,
message ,
default : defaultValue ,
} ,
] ) ;
return confirmed ;
}
/ * *
* Display installation summary
* @ param { Object } result - Installation result
* /
showInstallSummary ( result ) {
2025-12-15 15:55:28 +08:00
// Clean, simple completion message
console . log ( '\n' + chalk . green . bold ( '✨ BMAD is ready to use!' ) ) ;
2025-09-28 23:17:07 -05:00
2025-12-15 15:55:28 +08:00
// Show installation summary in a simple format
console . log ( chalk . dim ( ` Installed to: ${ result . path } ` ) ) ;
if ( result . modules && result . modules . length > 0 ) {
console . log ( chalk . dim ( ` Modules: ${ result . modules . join ( ', ' ) } ` ) ) ;
}
2025-12-05 17:54:03 -07:00
if ( result . agentVibesEnabled ) {
2025-12-15 15:55:28 +08:00
console . log ( chalk . dim ( ` TTS: Enabled ` ) ) ;
2025-12-05 17:54:03 -07:00
}
2025-12-15 15:55:28 +08:00
// TTS injection info (simplified)
2025-12-05 17:54:03 -07:00
if ( result . ttsInjectedFiles && result . ttsInjectedFiles . length > 0 ) {
2025-12-15 15:55:28 +08:00
console . log ( chalk . dim ( ` \n 💡 TTS enabled for ${ result . ttsInjectedFiles . length } agent(s) ` ) ) ;
console . log ( chalk . dim ( ' Agents will now speak when using AgentVibes' ) ) ;
2025-12-05 17:54:03 -07:00
}
2025-12-15 15:55:28 +08:00
console . log ( chalk . yellow ( '\nThank you for helping test the early release version of the new BMad Core and BMad Method!' ) ) ;
console . log ( chalk . cyan ( 'Stable Beta coming soon - please read the full README.md and linked documentation to get started!' ) ) ;
2025-12-16 18:22:46 +08:00
// Add changelog link at the end
console . log (
chalk . magenta (
"\n📋 Want to see what's new? Check out the changelog: https://github.com/bmad-code-org/BMAD-METHOD/blob/main/CHANGELOG.md" ,
) ,
) ;
2025-09-28 23:17:07 -05:00
}
/ * *
* Get confirmed directory from user
* @ returns { string } Confirmed directory path
* /
async getConfirmedDirectory ( ) {
let confirmedDirectory = null ;
while ( ! confirmedDirectory ) {
const directoryAnswer = await this . promptForDirectory ( ) ;
await this . displayDirectoryInfo ( directoryAnswer . directory ) ;
if ( await this . confirmDirectory ( directoryAnswer . directory ) ) {
confirmedDirectory = directoryAnswer . directory ;
}
}
return confirmedDirectory ;
}
/ * *
* Get existing installation info and installed modules
* @ param { string } directory - Installation directory
2025-12-23 20:04:42 +08:00
* @ returns { Object } Object with existingInstall , installedModuleIds , and bmadDir
2025-09-28 23:17:07 -05:00
* /
async getExistingInstallation ( directory ) {
const { Detector } = require ( '../installers/lib/core/detector' ) ;
2025-11-12 22:40:45 -06:00
const { Installer } = require ( '../installers/lib/core/installer' ) ;
2025-09-28 23:17:07 -05:00
const detector = new Detector ( ) ;
2025-11-12 22:40:45 -06:00
const installer = new Installer ( ) ;
2025-12-15 16:25:01 +08:00
const bmadDirResult = await installer . findBmadDir ( directory ) ;
2025-12-23 20:04:42 +08:00
const bmadDir = bmadDirResult . bmadDir ;
const existingInstall = await detector . detect ( bmadDir ) ;
2025-09-28 23:17:07 -05:00
const installedModuleIds = new Set ( existingInstall . modules . map ( ( mod ) => mod . id ) ) ;
2025-12-23 20:04:42 +08:00
return { existingInstall , installedModuleIds , bmadDir } ;
2025-09-28 23:17:07 -05:00
}
/ * *
* Collect core configuration
* @ param { string } directory - Installation directory
* @ returns { Object } Core configuration
* /
async collectCoreConfig ( directory ) {
const { ConfigCollector } = require ( '../installers/lib/core/config-collector' ) ;
const configCollector = new ConfigCollector ( ) ;
// Load existing configs first if they exist
await configCollector . loadExistingConfig ( directory ) ;
// Now collect with existing values as defaults (false = don't skip loading, true = skip completion message)
await configCollector . collectModuleConfig ( 'core' , directory , false , true ) ;
2025-12-15 12:55:57 +08:00
const coreConfig = configCollector . collectedConfig . core ;
// Ensure we always have a core config object, even if empty
return coreConfig || { } ;
2025-09-28 23:17:07 -05:00
}
/ * *
* Get module choices for selection
* @ param { Set } installedModuleIds - Currently installed module IDs
2025-12-07 13:39:03 -06:00
* @ param { Object } customContentConfig - Custom content configuration
2025-09-28 23:17:07 -05:00
* @ returns { Array } Module choices for inquirer
* /
2025-12-07 13:39:03 -06:00
async getModuleChoices ( installedModuleIds , customContentConfig = null ) {
const moduleChoices = [ ] ;
const isNewInstallation = installedModuleIds . size === 0 ;
2025-12-07 15:38:49 -06:00
const customContentItems = [ ] ;
const hasCustomContentItems = false ;
2025-12-07 13:39:03 -06:00
2025-12-07 20:46:09 -06:00
// Add custom content items
2025-12-13 17:50:33 +08:00
if ( customContentConfig && customContentConfig . hasCustomContent && customContentConfig . customPath ) {
// Existing installation - show from directory
const customHandler = new CustomHandler ( ) ;
const customFiles = await customHandler . findCustomContent ( customContentConfig . customPath ) ;
for ( const customFile of customFiles ) {
const customInfo = await customHandler . getCustomInfo ( customFile ) ;
if ( customInfo ) {
customContentItems . push ( {
name : ` ${ chalk . cyan ( '✓' ) } ${ customInfo . name } ${ chalk . gray ( ` ( ${ customInfo . relativePath } ) ` ) } ` ,
value : ` __CUSTOM_CONTENT__ ${ customFile } ` , // Unique value for each custom content
checked : true , // Default to selected since user chose to provide custom content
path : customInfo . path , // Track path to avoid duplicates
} ) ;
2025-12-07 13:39:03 -06:00
}
}
}
// Add official modules
2025-09-28 23:17:07 -05:00
const { ModuleManager } = require ( '../installers/lib/modules/manager' ) ;
2025-12-15 23:53:26 +08:00
const moduleManager = new ModuleManager ( ) ;
const { modules : availableModules , customModules : customModulesFromCache } = await moduleManager . listAvailable ( ) ;
2025-12-07 15:38:49 -06:00
// First, add all items to appropriate sections
const allCustomModules = [ ] ;
// Add custom content items from directory
allCustomModules . push ( ... customContentItems ) ;
2025-09-28 23:17:07 -05:00
2025-12-15 23:53:26 +08:00
// Add custom modules from cache
for ( const mod of customModulesFromCache ) {
2025-12-07 15:38:49 -06:00
// Skip if this module is already in customContentItems (by path)
const isDuplicate = allCustomModules . some ( ( item ) => item . path && mod . path && path . resolve ( item . path ) === path . resolve ( mod . path ) ) ;
if ( ! isDuplicate ) {
allCustomModules . push ( {
2025-12-15 23:53:26 +08:00
name : ` ${ chalk . cyan ( '✓' ) } ${ mod . name } ${ chalk . gray ( ` (cached) ` ) } ` ,
2025-12-07 15:38:49 -06:00
value : mod . id ,
checked : isNewInstallation ? mod . defaultSelected || false : installedModuleIds . has ( mod . id ) ,
} ) ;
}
}
// Add separators and modules in correct order
if ( allCustomModules . length > 0 ) {
// Add separator for custom content, all custom modules, and official content separator
moduleChoices . push (
new inquirer . Separator ( '── Custom Content ──' ) ,
... allCustomModules ,
new inquirer . Separator ( '── Official Content ──' ) ,
) ;
}
// Add official modules (only non-custom ones)
2025-12-07 13:39:03 -06:00
for ( const mod of availableModules ) {
2025-12-07 15:38:49 -06:00
if ( ! mod . isCustom ) {
moduleChoices . push ( {
name : mod . name ,
value : mod . id ,
checked : isNewInstallation ? mod . defaultSelected || false : installedModuleIds . has ( mod . id ) ,
} ) ;
}
2025-12-07 13:39:03 -06:00
}
2025-12-06 22:45:02 -06:00
return moduleChoices ;
2025-09-28 23:17:07 -05:00
}
/ * *
* Prompt for module selection
* @ param { Array } moduleChoices - Available module choices
* @ returns { Array } Selected module IDs
* /
2025-12-15 17:30:12 +08:00
async selectModules ( moduleChoices , defaultSelections = [ ] ) {
2025-09-28 23:17:07 -05:00
const moduleAnswer = await inquirer . prompt ( [
{
type : 'checkbox' ,
name : 'modules' ,
message : 'Select modules to install:' ,
choices : moduleChoices ,
2025-12-15 17:30:12 +08:00
default : defaultSelections ,
2025-09-28 23:17:07 -05:00
} ,
] ) ;
2025-12-15 23:53:26 +08:00
const selected = moduleAnswer . modules || [ ] ;
return selected ;
2025-09-28 23:17:07 -05:00
}
/ * *
* Prompt for directory selection
* @ returns { Object } Directory answer from inquirer
* /
async promptForDirectory ( ) {
return await inquirer . prompt ( [
{
type : 'input' ,
name : 'directory' ,
message : ` Installation directory: ` ,
default : process . cwd ( ) ,
validate : async ( input ) => this . validateDirectory ( input ) ,
filter : ( input ) => {
// If empty, use the default
if ( ! input || input . trim ( ) === '' ) {
return process . cwd ( ) ;
}
return this . expandUserPath ( input ) ;
} ,
} ,
] ) ;
}
/ * *
* Display directory information
* @ param { string } directory - The directory path
* /
async displayDirectoryInfo ( directory ) {
console . log ( chalk . cyan ( '\nResolved installation path:' ) , chalk . bold ( directory ) ) ;
const dirExists = await fs . pathExists ( directory ) ;
if ( dirExists ) {
// Show helpful context about the existing path
const stats = await fs . stat ( directory ) ;
if ( stats . isDirectory ( ) ) {
const files = await fs . readdir ( directory ) ;
if ( files . length > 0 ) {
2025-12-13 19:41:09 +08:00
// Check for any bmad installation (any folder with _config/manifest.yaml)
2025-11-12 22:40:45 -06:00
const { Installer } = require ( '../installers/lib/core/installer' ) ;
const installer = new Installer ( ) ;
2025-12-15 16:25:01 +08:00
const bmadResult = await installer . findBmadDir ( directory ) ;
const hasBmadInstall =
( await fs . pathExists ( bmadResult . bmadDir ) ) && ( await fs . pathExists ( path . join ( bmadResult . bmadDir , '_config' , 'manifest.yaml' ) ) ) ;
2025-11-12 22:40:45 -06:00
2025-09-28 23:17:07 -05:00
console . log (
chalk . gray ( ` Directory exists and contains ${ files . length } item(s) ` ) +
2025-12-15 16:25:01 +08:00
( hasBmadInstall ? chalk . yellow ( ` including existing BMAD installation ( ${ path . basename ( bmadResult . bmadDir ) } ) ` ) : '' ) ,
2025-09-28 23:17:07 -05:00
) ;
} else {
console . log ( chalk . gray ( 'Directory exists and is empty' ) ) ;
}
}
}
}
/ * *
* Confirm directory selection
* @ param { string } directory - The directory path
* @ returns { boolean } Whether user confirmed
* /
async confirmDirectory ( directory ) {
const dirExists = await fs . pathExists ( directory ) ;
if ( dirExists ) {
const confirmAnswer = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'proceed' ,
message : ` Install to this directory? ` ,
default : true ,
} ,
] ) ;
if ( ! confirmAnswer . proceed ) {
console . log ( chalk . yellow ( "\nLet's try again with a different path.\n" ) ) ;
}
return confirmAnswer . proceed ;
} else {
// Ask for confirmation to create the directory
const createConfirm = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'create' ,
message : ` The directory ' ${ directory } ' doesn't exist. Would you like to create it? ` ,
default : false ,
} ,
] ) ;
if ( ! createConfirm . create ) {
console . log ( chalk . yellow ( "\nLet's try again with a different path.\n" ) ) ;
}
return createConfirm . create ;
}
}
/ * *
* Validate directory path for installation
* @ param { string } input - User input path
* @ returns { string | true } Error message or true if valid
* /
async validateDirectory ( input ) {
// Allow empty input to use the default
if ( ! input || input . trim ( ) === '' ) {
return true ; // Empty means use default
}
let expandedPath ;
try {
expandedPath = this . expandUserPath ( input . trim ( ) ) ;
} catch ( error ) {
return error . message ;
}
// Check if the path exists
const pathExists = await fs . pathExists ( expandedPath ) ;
if ( ! pathExists ) {
// Find the first existing parent directory
const existingParent = await this . findExistingParent ( expandedPath ) ;
if ( ! existingParent ) {
return 'Cannot create directory: no existing parent directory found' ;
}
// Check if the existing parent is writable
try {
await fs . access ( existingParent , fs . constants . W _OK ) ;
// Path doesn't exist but can be created - will prompt for confirmation later
return true ;
} catch {
// Provide a detailed error message explaining both issues
return ` Directory ' ${ expandedPath } ' does not exist and cannot be created: parent directory ' ${ existingParent } ' is not writable ` ;
}
}
// If it exists, validate it's a directory and writable
const stat = await fs . stat ( expandedPath ) ;
if ( ! stat . isDirectory ( ) ) {
return ` Path exists but is not a directory: ${ expandedPath } ` ;
}
// Check write permissions
try {
await fs . access ( expandedPath , fs . constants . W _OK ) ;
} catch {
return ` Directory is not writable: ${ expandedPath } ` ;
}
return true ;
}
/ * *
* Find the first existing parent directory
* @ param { string } targetPath - The path to check
* @ returns { string | null } The first existing parent directory , or null if none found
* /
async findExistingParent ( targetPath ) {
let currentPath = path . resolve ( targetPath ) ;
// Walk up the directory tree until we find an existing directory
while ( currentPath !== path . dirname ( currentPath ) ) {
// Stop at root
const parent = path . dirname ( currentPath ) ;
if ( await fs . pathExists ( parent ) ) {
return parent ;
}
currentPath = parent ;
}
return null ; // No existing parent found (shouldn't happen in practice)
}
/ * *
* Expands the user - provided path : handles ~ and resolves to absolute .
* @ param { string } inputPath - User input path .
* @ returns { string } Absolute expanded path .
* /
expandUserPath ( inputPath ) {
if ( typeof inputPath !== 'string' ) {
throw new TypeError ( 'Path must be a string.' ) ;
}
let expanded = inputPath . trim ( ) ;
// Handle tilde expansion
if ( expanded . startsWith ( '~' ) ) {
if ( expanded === '~' ) {
expanded = os . homedir ( ) ;
} else if ( expanded . startsWith ( '~' + path . sep ) ) {
const pathAfterHome = expanded . slice ( 2 ) ; // Remove ~/ or ~\
expanded = path . join ( os . homedir ( ) , pathAfterHome ) ;
} else {
const restOfPath = expanded . slice ( 1 ) ;
const separatorIndex = restOfPath . indexOf ( path . sep ) ;
const username = separatorIndex === - 1 ? restOfPath : restOfPath . slice ( 0 , separatorIndex ) ;
if ( username ) {
throw new Error ( ` Path expansion for ~ ${ username } is not supported. Please use an absolute path or ~ ${ path . sep } ` ) ;
}
}
}
// Resolve to the absolute path relative to the current working directory
return path . resolve ( expanded ) ;
}
Add Text-to-Speech Integration via TTS_INJECTION System (#934)
* feat: Add provider-agnostic TTS integration via injection point system
Implements comprehensive Text-to-Speech integration for BMAD agents using a generic
TTS_INJECTION marker system. When AgentVibes (or any compatible TTS provider) is
installed, all BMAD agents can speak their responses with unique AI voices.
## Key Features
**Provider-Agnostic Architecture**
- Uses generic `TTS_INJECTION` markers instead of vendor-specific naming
- Future-proof for multiple TTS providers beyond AgentVibes
- Clean separation - BMAD stays TTS-agnostic, providers handle injection
**Installation Flow**
- BMAD → AgentVibes: TTS instructions injected when AgentVibes detects existing BMAD installation
- AgentVibes → BMAD: TTS instructions injected during BMAD installation when AgentVibes detected
- User must manually create voice assignment file when AgentVibes installs first (documented limitation)
**Party Mode Voice Support**
- Each agent speaks with unique assigned voice in multi-agent discussions
- PM, Architect, Developer, Analyst, UX Designer, etc. - all with distinct voices
**Zero Breaking Changes**
- Fully backward compatible - works without any TTS provider
- `TTS_INJECTION` markers are benign HTML comments if not processed
- No changes to existing agent behavior or non-TTS workflows
## Implementation Details
**Files Modified:**
- `tools/cli/installers/lib/core/installer.js` - TTS injection processing logic
- `tools/cli/lib/ui.js` - AgentVibes detection and installation prompts
- `tools/cli/commands/install.js` - Post-install guidance for AgentVibes setup
- `src/utility/models/fragments/activation-rules.xml` - TTS_INJECTION marker for agents
- `src/core/workflows/party-mode/instructions.md` - TTS_INJECTION marker for party mode
**Injection Point System:**
```xml
<rules>
- ALWAYS communicate in {communication_language}
<!-- TTS_INJECTION:agent-tts -->
- Stay in character until exit selected
</rules>
```
When AgentVibes is detected, the installer replaces this marker with:
```
- When responding to user messages, speak your responses using TTS:
Call: `.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'` after each response
IMPORTANT: Use single quotes - do NOT escape special characters like ! or $
```
**Special Character Handling:**
- Explicit guidance to use single quotes without escaping
- Prevents "backslash exclamation" artifacts in speech
**User Experience:**
```
User: "How should we architect this feature?"
Architect: [Text response] + 🔊 [Professional voice explains architecture]
```
Party Mode:
```
PM (John): "I'll focus on user value..." 🔊 [Male professional voice]
UX Designer (Sara): "From a user perspective..." 🔊 [Female voice]
Architect (Marcus): "The technical approach..." 🔊 [Male technical voice]
```
## Testing
**Unit Tests:** ✅ 62/62 passing
- 49/49 schema validation tests
- 13/13 installation component tests
**Integration Testing:**
- ✅ BMAD → AgentVibes (automatic injection)
- ✅ AgentVibes → BMAD (automatic injection)
- ✅ No TTS provider (markers remain as comments)
## Documentation
Comprehensive testing guide created with:
- Both installation scenario walkthroughs
- Verification commands and expected outputs
- Troubleshooting guidance
## Known Limitations
**AgentVibes → BMAD Installation Order:**
When AgentVibes installs first, voice assignment file must be created manually:
```bash
mkdir -p .bmad/_cfg
cat > .bmad/_cfg/agent-voice-map.csv << 'EOF'
agent_id,voice_name
pm,en_US-ryan-high
architect,en_US-danny-low
dev,en_US-joe-medium
EOF
```
This limitation exists to prevent false legacy v4 detection warnings from BMAD installer.
**Recommended:** Install BMAD first, then AgentVibes for automatic voice assignment.
## Related Work
**Companion Implementation:**
- Repository: paulpreibisch/AgentVibes
- Commits: 6 commits implementing injection processing and voice routing
- Features: Retroactive injection, file path extraction, escape stripping
**GitHub Issues:**
- paulpreibisch/AgentVibes#36 - BMAD agent ID support
## Breaking Changes
None. Feature is opt-in and requires separate TTS provider installation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Enforce project hooks over global hooks in party mode
before, claude would sometimes favor global agent vibes hooks over project specific
* feat: Automate AgentVibes installer invocation after BMAD install
Instead of showing manual installation instructions, the installer now:
- Prompts "Press Enter to start AgentVibes installer..."
- Automatically runs npx agentvibes@latest install
- Handles errors gracefully with fallback instructions
This provides a seamless installation flow matching the test script's
interactive approach.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: Add automated testing script and guide for PR #934
Added comprehensive testing tools for AgentVibes party mode integration:
- test-bmad-pr.sh: Fully automated installation and verification script
- Interactive mode selection (official PR or custom fork)
- Automatic BMAD CLI setup and linking
- AgentVibes installation with guided prompts
- Built-in verification checks for voice maps and hooks
- Saved configuration for quick re-testing
- TESTING.md: Complete testing documentation
- Quick start with one-line npx command
- Manual installation alternative
- Troubleshooting guide
- Cleanup instructions
Testers can now run a single command to test the full AgentVibes integration
without needing to understand the complex setup process.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Add shell: true to npx execSync to prevent permission denied error
The execSync call for 'npx agentvibes@latest install' was failing with
'Permission denied' because the shell was trying to execute 'agentvibes@latest'
directly instead of passing it as an argument to npx.
Adding shell: true ensures the command runs in a proper shell context
where npx can correctly interpret the @latest version syntax.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Remove duplicate AgentVibes installation step from test script
The test script was calling AgentVibes installer twice:
1. BMAD installer now automatically runs AgentVibes (new feature)
2. Test script had a separate Step 6 that also ran AgentVibes
This caused the installer to run twice, with the second call failing
because it was already installed.
Changes:
- Removed redundant Step 6 (AgentVibes installation)
- Updated Step 5 to indicate it includes AgentVibes
- Updated step numbers from 7 to 6 throughout
- Added guidance that AgentVibes runs automatically
Now the flow is cleaner: BMAD installer handles everything!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Address bmadcode review - preserve variables and move TTS logic to injection
Fixes requested changes from PR review:
1. Preserve {bmad_folder} variable placeholder
- Changed: {project_root}/.bmad/core/tasks/workflow.xml
- To: {project_root}/{bmad_folder}/core/tasks/workflow.xml
- Allows users to choose custom BMAD folder names during installation
2. Move TTS-specific hook guidance to injection system
- Removed hardcoded hook enforcement from source files
- Added hook guidance to processTTSInjectionPoints() in installer.js
- Now only appears when AgentVibes is installed (via TTS_INJECTION)
3. Maintain TTS-agnostic source architecture
- Source files remain clean of TTS-specific instructions
- TTS details injected at install-time only when needed
- Preserves provider-agnostic design principle
Changes made:
- src/core/workflows/party-mode/instructions.md
- Reverted .bmad to {bmad_folder} variable
- Replaced hardcoded hook guidance with <!-- TTS_INJECTION:party-mode -->
- Removed <note> about play-tts.sh hook location
- tools/cli/installers/lib/core/installer.js
- Added hook enforcement to party-mode injection replacement
- Guidance now injected only when enableAgentVibes is true
Addresses review comments from bmadcode:
- "needs to remain the variable. it will get set in the file at the install destination."
- "items like this we will need to inject if user is using claude and TTS"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Change 'claude-code' to 'claude' in test script instructions
The correct command to start Claude is 'claude', not 'claude-code'.
Updated line 362-363 in test-bmad-pr.sh to show the correct command.
* fix: Remove npm link from test script to avoid global namespace pollution
- Removed 'npm link' command that was installing BMAD globally
- Changed 'bmad install' to direct node execution using local clone
- Updated success message to reflect no global installation
This keeps testing fully isolated and prevents conflicts with:
- Existing BMAD installations
- Future official BMAD installs
- Orphaned symlinks when test directory is deleted
The test script now runs completely self-contained without modifying
the user's global npm environment.
---------
Co-authored-by: Claude Code <claude@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Preibisch <paul@paulpreibisch.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2025-11-26 08:51:57 -07:00
/ * *
* @ function promptAgentVibes
* @ intent Ask user if they want AgentVibes TTS integration during BMAD installation
* @ why Enables optional voice features without forcing TTS on users who don ' t want it
* @ param { string } projectDir - Absolute path to user ' s project directory
* @ returns { Promise < Object > } Configuration object : { enabled : boolean , alreadyInstalled : boolean }
* @ sideeffects None - pure user input collection , no files written
* @ edgecases Shows warning if user enables TTS but AgentVibes not detected
* @ calledby promptInstall ( ) during installation flow , after core config , before IDE selection
* @ calls checkAgentVibesInstalled ( ) , inquirer . prompt ( ) , chalk . green / yellow / dim ( )
*
* AI NOTE : This prompt is strategically positioned in installation flow :
2025-12-10 20:50:24 +09:00
* - AFTER core config ( user _name , etc )
Add Text-to-Speech Integration via TTS_INJECTION System (#934)
* feat: Add provider-agnostic TTS integration via injection point system
Implements comprehensive Text-to-Speech integration for BMAD agents using a generic
TTS_INJECTION marker system. When AgentVibes (or any compatible TTS provider) is
installed, all BMAD agents can speak their responses with unique AI voices.
## Key Features
**Provider-Agnostic Architecture**
- Uses generic `TTS_INJECTION` markers instead of vendor-specific naming
- Future-proof for multiple TTS providers beyond AgentVibes
- Clean separation - BMAD stays TTS-agnostic, providers handle injection
**Installation Flow**
- BMAD → AgentVibes: TTS instructions injected when AgentVibes detects existing BMAD installation
- AgentVibes → BMAD: TTS instructions injected during BMAD installation when AgentVibes detected
- User must manually create voice assignment file when AgentVibes installs first (documented limitation)
**Party Mode Voice Support**
- Each agent speaks with unique assigned voice in multi-agent discussions
- PM, Architect, Developer, Analyst, UX Designer, etc. - all with distinct voices
**Zero Breaking Changes**
- Fully backward compatible - works without any TTS provider
- `TTS_INJECTION` markers are benign HTML comments if not processed
- No changes to existing agent behavior or non-TTS workflows
## Implementation Details
**Files Modified:**
- `tools/cli/installers/lib/core/installer.js` - TTS injection processing logic
- `tools/cli/lib/ui.js` - AgentVibes detection and installation prompts
- `tools/cli/commands/install.js` - Post-install guidance for AgentVibes setup
- `src/utility/models/fragments/activation-rules.xml` - TTS_INJECTION marker for agents
- `src/core/workflows/party-mode/instructions.md` - TTS_INJECTION marker for party mode
**Injection Point System:**
```xml
<rules>
- ALWAYS communicate in {communication_language}
<!-- TTS_INJECTION:agent-tts -->
- Stay in character until exit selected
</rules>
```
When AgentVibes is detected, the installer replaces this marker with:
```
- When responding to user messages, speak your responses using TTS:
Call: `.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'` after each response
IMPORTANT: Use single quotes - do NOT escape special characters like ! or $
```
**Special Character Handling:**
- Explicit guidance to use single quotes without escaping
- Prevents "backslash exclamation" artifacts in speech
**User Experience:**
```
User: "How should we architect this feature?"
Architect: [Text response] + 🔊 [Professional voice explains architecture]
```
Party Mode:
```
PM (John): "I'll focus on user value..." 🔊 [Male professional voice]
UX Designer (Sara): "From a user perspective..." 🔊 [Female voice]
Architect (Marcus): "The technical approach..." 🔊 [Male technical voice]
```
## Testing
**Unit Tests:** ✅ 62/62 passing
- 49/49 schema validation tests
- 13/13 installation component tests
**Integration Testing:**
- ✅ BMAD → AgentVibes (automatic injection)
- ✅ AgentVibes → BMAD (automatic injection)
- ✅ No TTS provider (markers remain as comments)
## Documentation
Comprehensive testing guide created with:
- Both installation scenario walkthroughs
- Verification commands and expected outputs
- Troubleshooting guidance
## Known Limitations
**AgentVibes → BMAD Installation Order:**
When AgentVibes installs first, voice assignment file must be created manually:
```bash
mkdir -p .bmad/_cfg
cat > .bmad/_cfg/agent-voice-map.csv << 'EOF'
agent_id,voice_name
pm,en_US-ryan-high
architect,en_US-danny-low
dev,en_US-joe-medium
EOF
```
This limitation exists to prevent false legacy v4 detection warnings from BMAD installer.
**Recommended:** Install BMAD first, then AgentVibes for automatic voice assignment.
## Related Work
**Companion Implementation:**
- Repository: paulpreibisch/AgentVibes
- Commits: 6 commits implementing injection processing and voice routing
- Features: Retroactive injection, file path extraction, escape stripping
**GitHub Issues:**
- paulpreibisch/AgentVibes#36 - BMAD agent ID support
## Breaking Changes
None. Feature is opt-in and requires separate TTS provider installation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Enforce project hooks over global hooks in party mode
before, claude would sometimes favor global agent vibes hooks over project specific
* feat: Automate AgentVibes installer invocation after BMAD install
Instead of showing manual installation instructions, the installer now:
- Prompts "Press Enter to start AgentVibes installer..."
- Automatically runs npx agentvibes@latest install
- Handles errors gracefully with fallback instructions
This provides a seamless installation flow matching the test script's
interactive approach.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: Add automated testing script and guide for PR #934
Added comprehensive testing tools for AgentVibes party mode integration:
- test-bmad-pr.sh: Fully automated installation and verification script
- Interactive mode selection (official PR or custom fork)
- Automatic BMAD CLI setup and linking
- AgentVibes installation with guided prompts
- Built-in verification checks for voice maps and hooks
- Saved configuration for quick re-testing
- TESTING.md: Complete testing documentation
- Quick start with one-line npx command
- Manual installation alternative
- Troubleshooting guide
- Cleanup instructions
Testers can now run a single command to test the full AgentVibes integration
without needing to understand the complex setup process.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Add shell: true to npx execSync to prevent permission denied error
The execSync call for 'npx agentvibes@latest install' was failing with
'Permission denied' because the shell was trying to execute 'agentvibes@latest'
directly instead of passing it as an argument to npx.
Adding shell: true ensures the command runs in a proper shell context
where npx can correctly interpret the @latest version syntax.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Remove duplicate AgentVibes installation step from test script
The test script was calling AgentVibes installer twice:
1. BMAD installer now automatically runs AgentVibes (new feature)
2. Test script had a separate Step 6 that also ran AgentVibes
This caused the installer to run twice, with the second call failing
because it was already installed.
Changes:
- Removed redundant Step 6 (AgentVibes installation)
- Updated Step 5 to indicate it includes AgentVibes
- Updated step numbers from 7 to 6 throughout
- Added guidance that AgentVibes runs automatically
Now the flow is cleaner: BMAD installer handles everything!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Address bmadcode review - preserve variables and move TTS logic to injection
Fixes requested changes from PR review:
1. Preserve {bmad_folder} variable placeholder
- Changed: {project_root}/.bmad/core/tasks/workflow.xml
- To: {project_root}/{bmad_folder}/core/tasks/workflow.xml
- Allows users to choose custom BMAD folder names during installation
2. Move TTS-specific hook guidance to injection system
- Removed hardcoded hook enforcement from source files
- Added hook guidance to processTTSInjectionPoints() in installer.js
- Now only appears when AgentVibes is installed (via TTS_INJECTION)
3. Maintain TTS-agnostic source architecture
- Source files remain clean of TTS-specific instructions
- TTS details injected at install-time only when needed
- Preserves provider-agnostic design principle
Changes made:
- src/core/workflows/party-mode/instructions.md
- Reverted .bmad to {bmad_folder} variable
- Replaced hardcoded hook guidance with <!-- TTS_INJECTION:party-mode -->
- Removed <note> about play-tts.sh hook location
- tools/cli/installers/lib/core/installer.js
- Added hook enforcement to party-mode injection replacement
- Guidance now injected only when enableAgentVibes is true
Addresses review comments from bmadcode:
- "needs to remain the variable. it will get set in the file at the install destination."
- "items like this we will need to inject if user is using claude and TTS"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Change 'claude-code' to 'claude' in test script instructions
The correct command to start Claude is 'claude', not 'claude-code'.
Updated line 362-363 in test-bmad-pr.sh to show the correct command.
* fix: Remove npm link from test script to avoid global namespace pollution
- Removed 'npm link' command that was installing BMAD globally
- Changed 'bmad install' to direct node execution using local clone
- Updated success message to reflect no global installation
This keeps testing fully isolated and prevents conflicts with:
- Existing BMAD installations
- Future official BMAD installs
- Orphaned symlinks when test directory is deleted
The test script now runs completely self-contained without modifying
the user's global npm environment.
---------
Co-authored-by: Claude Code <claude@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Preibisch <paul@paulpreibisch.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2025-11-26 08:51:57 -07:00
* - BEFORE IDE selection ( which can hang on Windows / PowerShell )
*
* Flow Logic :
* 1. Auto - detect if AgentVibes already installed ( checks for hook files )
* 2. Show detection status to user ( green checkmark or gray "not detected" )
* 3. Prompt : "Enable AgentVibes TTS?" ( defaults to true if detected )
* 4. If user says YES but AgentVibes NOT installed :
* → Show warning with installation link ( graceful degradation )
* 5. Return config to promptInstall ( ) , which passes to installer . install ( )
*
* State Flow :
* promptAgentVibes ( ) → { enabled , alreadyInstalled }
* ↓
* promptInstall ( ) → config . enableAgentVibes
* ↓
* installer . install ( ) → this . enableAgentVibes
* ↓
* processTTSInjectionPoints ( ) → injects OR strips markers
*
* RELATED :
* === === ==
* - Detection : checkAgentVibesInstalled ( ) - looks for bmad - speak . sh and play - tts . sh
* - Processing : installer . js : : processTTSInjectionPoints ( )
* - Markers : src / core / workflows / party - mode / instructions . md : 101 , src / modules / bmm / agents / * . md
* - GitHub Issue : paulpreibisch / AgentVibes # 36
* /
async promptAgentVibes ( projectDir ) {
CLIUtils . displaySection ( '🎤 Voice Features' , 'Enable TTS for multi-agent conversations' ) ;
// Check if AgentVibes is already installed
const agentVibesInstalled = await this . checkAgentVibesInstalled ( projectDir ) ;
if ( agentVibesInstalled ) {
console . log ( chalk . green ( ' ✓ AgentVibes detected' ) ) ;
} else {
console . log ( chalk . dim ( ' AgentVibes not detected' ) ) ;
}
const answers = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'enableTts' ,
2025-11-26 17:46:26 -06:00
message : 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)' ,
default : false , // Default to yes - recommended for best experience
Add Text-to-Speech Integration via TTS_INJECTION System (#934)
* feat: Add provider-agnostic TTS integration via injection point system
Implements comprehensive Text-to-Speech integration for BMAD agents using a generic
TTS_INJECTION marker system. When AgentVibes (or any compatible TTS provider) is
installed, all BMAD agents can speak their responses with unique AI voices.
## Key Features
**Provider-Agnostic Architecture**
- Uses generic `TTS_INJECTION` markers instead of vendor-specific naming
- Future-proof for multiple TTS providers beyond AgentVibes
- Clean separation - BMAD stays TTS-agnostic, providers handle injection
**Installation Flow**
- BMAD → AgentVibes: TTS instructions injected when AgentVibes detects existing BMAD installation
- AgentVibes → BMAD: TTS instructions injected during BMAD installation when AgentVibes detected
- User must manually create voice assignment file when AgentVibes installs first (documented limitation)
**Party Mode Voice Support**
- Each agent speaks with unique assigned voice in multi-agent discussions
- PM, Architect, Developer, Analyst, UX Designer, etc. - all with distinct voices
**Zero Breaking Changes**
- Fully backward compatible - works without any TTS provider
- `TTS_INJECTION` markers are benign HTML comments if not processed
- No changes to existing agent behavior or non-TTS workflows
## Implementation Details
**Files Modified:**
- `tools/cli/installers/lib/core/installer.js` - TTS injection processing logic
- `tools/cli/lib/ui.js` - AgentVibes detection and installation prompts
- `tools/cli/commands/install.js` - Post-install guidance for AgentVibes setup
- `src/utility/models/fragments/activation-rules.xml` - TTS_INJECTION marker for agents
- `src/core/workflows/party-mode/instructions.md` - TTS_INJECTION marker for party mode
**Injection Point System:**
```xml
<rules>
- ALWAYS communicate in {communication_language}
<!-- TTS_INJECTION:agent-tts -->
- Stay in character until exit selected
</rules>
```
When AgentVibes is detected, the installer replaces this marker with:
```
- When responding to user messages, speak your responses using TTS:
Call: `.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'` after each response
IMPORTANT: Use single quotes - do NOT escape special characters like ! or $
```
**Special Character Handling:**
- Explicit guidance to use single quotes without escaping
- Prevents "backslash exclamation" artifacts in speech
**User Experience:**
```
User: "How should we architect this feature?"
Architect: [Text response] + 🔊 [Professional voice explains architecture]
```
Party Mode:
```
PM (John): "I'll focus on user value..." 🔊 [Male professional voice]
UX Designer (Sara): "From a user perspective..." 🔊 [Female voice]
Architect (Marcus): "The technical approach..." 🔊 [Male technical voice]
```
## Testing
**Unit Tests:** ✅ 62/62 passing
- 49/49 schema validation tests
- 13/13 installation component tests
**Integration Testing:**
- ✅ BMAD → AgentVibes (automatic injection)
- ✅ AgentVibes → BMAD (automatic injection)
- ✅ No TTS provider (markers remain as comments)
## Documentation
Comprehensive testing guide created with:
- Both installation scenario walkthroughs
- Verification commands and expected outputs
- Troubleshooting guidance
## Known Limitations
**AgentVibes → BMAD Installation Order:**
When AgentVibes installs first, voice assignment file must be created manually:
```bash
mkdir -p .bmad/_cfg
cat > .bmad/_cfg/agent-voice-map.csv << 'EOF'
agent_id,voice_name
pm,en_US-ryan-high
architect,en_US-danny-low
dev,en_US-joe-medium
EOF
```
This limitation exists to prevent false legacy v4 detection warnings from BMAD installer.
**Recommended:** Install BMAD first, then AgentVibes for automatic voice assignment.
## Related Work
**Companion Implementation:**
- Repository: paulpreibisch/AgentVibes
- Commits: 6 commits implementing injection processing and voice routing
- Features: Retroactive injection, file path extraction, escape stripping
**GitHub Issues:**
- paulpreibisch/AgentVibes#36 - BMAD agent ID support
## Breaking Changes
None. Feature is opt-in and requires separate TTS provider installation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Enforce project hooks over global hooks in party mode
before, claude would sometimes favor global agent vibes hooks over project specific
* feat: Automate AgentVibes installer invocation after BMAD install
Instead of showing manual installation instructions, the installer now:
- Prompts "Press Enter to start AgentVibes installer..."
- Automatically runs npx agentvibes@latest install
- Handles errors gracefully with fallback instructions
This provides a seamless installation flow matching the test script's
interactive approach.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: Add automated testing script and guide for PR #934
Added comprehensive testing tools for AgentVibes party mode integration:
- test-bmad-pr.sh: Fully automated installation and verification script
- Interactive mode selection (official PR or custom fork)
- Automatic BMAD CLI setup and linking
- AgentVibes installation with guided prompts
- Built-in verification checks for voice maps and hooks
- Saved configuration for quick re-testing
- TESTING.md: Complete testing documentation
- Quick start with one-line npx command
- Manual installation alternative
- Troubleshooting guide
- Cleanup instructions
Testers can now run a single command to test the full AgentVibes integration
without needing to understand the complex setup process.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Add shell: true to npx execSync to prevent permission denied error
The execSync call for 'npx agentvibes@latest install' was failing with
'Permission denied' because the shell was trying to execute 'agentvibes@latest'
directly instead of passing it as an argument to npx.
Adding shell: true ensures the command runs in a proper shell context
where npx can correctly interpret the @latest version syntax.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Remove duplicate AgentVibes installation step from test script
The test script was calling AgentVibes installer twice:
1. BMAD installer now automatically runs AgentVibes (new feature)
2. Test script had a separate Step 6 that also ran AgentVibes
This caused the installer to run twice, with the second call failing
because it was already installed.
Changes:
- Removed redundant Step 6 (AgentVibes installation)
- Updated Step 5 to indicate it includes AgentVibes
- Updated step numbers from 7 to 6 throughout
- Added guidance that AgentVibes runs automatically
Now the flow is cleaner: BMAD installer handles everything!
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Address bmadcode review - preserve variables and move TTS logic to injection
Fixes requested changes from PR review:
1. Preserve {bmad_folder} variable placeholder
- Changed: {project_root}/.bmad/core/tasks/workflow.xml
- To: {project_root}/{bmad_folder}/core/tasks/workflow.xml
- Allows users to choose custom BMAD folder names during installation
2. Move TTS-specific hook guidance to injection system
- Removed hardcoded hook enforcement from source files
- Added hook guidance to processTTSInjectionPoints() in installer.js
- Now only appears when AgentVibes is installed (via TTS_INJECTION)
3. Maintain TTS-agnostic source architecture
- Source files remain clean of TTS-specific instructions
- TTS details injected at install-time only when needed
- Preserves provider-agnostic design principle
Changes made:
- src/core/workflows/party-mode/instructions.md
- Reverted .bmad to {bmad_folder} variable
- Replaced hardcoded hook guidance with <!-- TTS_INJECTION:party-mode -->
- Removed <note> about play-tts.sh hook location
- tools/cli/installers/lib/core/installer.js
- Added hook enforcement to party-mode injection replacement
- Guidance now injected only when enableAgentVibes is true
Addresses review comments from bmadcode:
- "needs to remain the variable. it will get set in the file at the install destination."
- "items like this we will need to inject if user is using claude and TTS"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Change 'claude-code' to 'claude' in test script instructions
The correct command to start Claude is 'claude', not 'claude-code'.
Updated line 362-363 in test-bmad-pr.sh to show the correct command.
* fix: Remove npm link from test script to avoid global namespace pollution
- Removed 'npm link' command that was installing BMAD globally
- Changed 'bmad install' to direct node execution using local clone
- Updated success message to reflect no global installation
This keeps testing fully isolated and prevents conflicts with:
- Existing BMAD installations
- Future official BMAD installs
- Orphaned symlinks when test directory is deleted
The test script now runs completely self-contained without modifying
the user's global npm environment.
---------
Co-authored-by: Claude Code <claude@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Preibisch <paul@paulpreibisch.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2025-11-26 08:51:57 -07:00
} ,
] ) ;
if ( answers . enableTts && ! agentVibesInstalled ) {
console . log ( chalk . yellow ( '\n ⚠️ AgentVibes not installed' ) ) ;
console . log ( chalk . dim ( ' Install AgentVibes separately to enable TTS:' ) ) ;
console . log ( chalk . dim ( ' https://github.com/paulpreibisch/AgentVibes\n' ) ) ;
}
return {
enabled : answers . enableTts ,
alreadyInstalled : agentVibesInstalled ,
} ;
}
/ * *
* @ function checkAgentVibesInstalled
* @ intent Detect if AgentVibes TTS hooks are present in user ' s project
* @ why Allows auto - enabling TTS and showing helpful installation guidance
* @ param { string } projectDir - Absolute path to user ' s project directory
* @ returns { Promise < boolean > } true if both required AgentVibes hooks exist , false otherwise
* @ sideeffects None - read - only file existence checks
* @ edgecases Returns false if either hook missing ( both required for functional TTS )
* @ calledby promptAgentVibes ( ) to determine default value and show detection status
* @ calls fs . pathExists ( ) twice ( bmad - speak . sh , play - tts . sh )
*
* AI NOTE : This checks for the MINIMUM viable AgentVibes installation .
*
* Required Files :
* === === === === ===
* 1. . claude / hooks / bmad - speak . sh
* - Maps agent display names → agent IDs → voice profiles
* - Calls play - tts . sh with agent ' s assigned voice
* - Created by AgentVibes installer
*
* 2. . claude / hooks / play - tts . sh
* - Core TTS router ( ElevenLabs or Piper )
* - Provider - agnostic interface
* - Required by bmad - speak . sh
*
* Why Both Required :
* === === === === === ===
* - bmad - speak . sh alone : No TTS backend
* - play - tts . sh alone : No BMAD agent voice mapping
* - Both together : Full party mode TTS integration
*
* Detection Strategy :
* === === === === === === =
* We use simple file existence ( not version checks ) because :
* - Fast and reliable
* - Works across all AgentVibes versions
* - User will discover version issues when TTS runs ( fail - fast )
*
* PATTERN : Adding New Detection Criteria
* === === === === === === === === === === === === ===
* If future AgentVibes features require additional files :
* 1. Add new pathExists check to this function
* 2. Update documentation in promptAgentVibes ( )
* 3. Consider : should missing file prevent detection or just log warning ?
*
* RELATED :
* === === ==
* - AgentVibes Installer : creates these hooks
* - bmad - speak . sh : calls play - tts . sh with agent voices
* - Party Mode : uses bmad - speak . sh for agent dialogue
* /
async checkAgentVibesInstalled ( projectDir ) {
const fs = require ( 'fs-extra' ) ;
const path = require ( 'node:path' ) ;
// Check for AgentVibes hook files
const hookPath = path . join ( projectDir , '.claude' , 'hooks' , 'bmad-speak.sh' ) ;
const playTtsPath = path . join ( projectDir , '.claude' , 'hooks' , 'play-tts.sh' ) ;
return ( await fs . pathExists ( hookPath ) ) && ( await fs . pathExists ( playTtsPath ) ) ;
}
2025-12-07 20:46:09 -06:00
2025-12-15 17:30:12 +08:00
/ * *
* Load existing configurations to use as defaults
* @ param { string } directory - Installation directory
* @ returns { Object } Existing configurations
* /
async loadExistingConfigurations ( directory ) {
const configs = {
hasCustomContent : false ,
coreConfig : { } ,
ideConfig : { ides : [ ] , skipIde : false } ,
agentVibesConfig : { enabled : false , alreadyInstalled : false } ,
} ;
try {
// Load core config
configs . coreConfig = await this . collectCoreConfig ( directory ) ;
// Load IDE configuration
const configuredIdes = await this . getConfiguredIdes ( directory ) ;
if ( configuredIdes . length > 0 ) {
configs . ideConfig . ides = configuredIdes ;
configs . ideConfig . skipIde = false ;
}
// Load AgentVibes configuration
const agentVibesInstalled = await this . checkAgentVibesInstalled ( directory ) ;
configs . agentVibesConfig = { enabled : agentVibesInstalled , alreadyInstalled : agentVibesInstalled } ;
return configs ;
} catch {
// If loading fails, return empty configs
console . warn ( 'Warning: Could not load existing configurations' ) ;
return configs ;
}
}
/ * *
* Get configured IDEs from existing installation
* @ param { string } directory - Installation directory
* @ returns { Array } List of configured IDEs
* /
async getConfiguredIdes ( directory ) {
const { Detector } = require ( '../installers/lib/core/detector' ) ;
const { Installer } = require ( '../installers/lib/core/installer' ) ;
const detector = new Detector ( ) ;
const installer = new Installer ( ) ;
const bmadResult = await installer . findBmadDir ( directory ) ;
const existingInstall = await detector . detect ( bmadResult . bmadDir ) ;
return existingInstall . ides || [ ] ;
}
2025-12-14 10:03:25 +08:00
/ * *
* Prompt user for custom content source location
* @ returns { Object } Custom content configuration
* /
async promptCustomContentSource ( ) {
const customContentConfig = { hasCustomContent : true , sources : [ ] } ;
// Keep asking for more sources until user is done
while ( true ) {
2025-12-15 11:33:01 +08:00
// First ask if user wants to add another module or continue
if ( customContentConfig . sources . length > 0 ) {
const { action } = await inquirer . prompt ( [
{
type : 'list' ,
name : 'action' ,
message : 'Would you like to:' ,
choices : [
{ name : 'Add another custom module' , value : 'add' } ,
{ name : 'Continue with installation' , value : 'continue' } ,
] ,
default : 'continue' ,
} ,
] ) ;
if ( action === 'continue' ) {
break ;
}
}
2025-12-14 10:03:25 +08:00
let sourcePath ;
let isValid = false ;
while ( ! isValid ) {
const { path : inputPath } = await inquirer . prompt ( [
{
type : 'input' ,
name : 'path' ,
2025-12-15 11:33:01 +08:00
message : 'Enter the path to your custom content folder (or press Enter to cancel):' ,
2025-12-14 10:03:25 +08:00
validate : async ( input ) => {
2025-12-15 11:33:01 +08:00
// Allow empty input to cancel
2025-12-14 10:03:25 +08:00
if ( ! input || input . trim ( ) === '' ) {
2025-12-15 11:33:01 +08:00
return true ; // Allow empty to exit
2025-12-14 10:03:25 +08:00
}
try {
// Expand the path
const expandedPath = this . expandUserPath ( input . trim ( ) ) ;
// Check if path exists
if ( ! ( await fs . pathExists ( expandedPath ) ) ) {
return 'Path does not exist' ;
}
// Check if it's a directory
const stat = await fs . stat ( expandedPath ) ;
if ( ! stat . isDirectory ( ) ) {
return 'Path must be a directory' ;
}
// Check for module.yaml in the root
const moduleYamlPath = path . join ( expandedPath , 'module.yaml' ) ;
if ( ! ( await fs . pathExists ( moduleYamlPath ) ) ) {
return 'Directory must contain a module.yaml file in the root' ;
}
// Try to parse the module.yaml to get the module ID
try {
const yaml = require ( 'yaml' ) ;
const content = await fs . readFile ( moduleYamlPath , 'utf8' ) ;
const moduleData = yaml . parse ( content ) ;
if ( ! moduleData . code ) {
return 'module.yaml must contain a "code" field for the module ID' ;
}
} catch ( error ) {
return 'Invalid module.yaml file: ' + error . message ;
}
return true ;
} catch ( error ) {
return 'Error validating path: ' + error . message ;
}
} ,
} ,
] ) ;
2025-12-15 11:33:01 +08:00
// If user pressed Enter without typing anything, exit the loop
if ( ! inputPath || inputPath . trim ( ) === '' ) {
// If we have no modules yet, return false for no custom content
if ( customContentConfig . sources . length === 0 ) {
return { hasCustomContent : false } ;
}
return customContentConfig ;
}
2025-12-14 10:03:25 +08:00
sourcePath = this . expandUserPath ( inputPath ) ;
isValid = true ;
}
// Read module.yaml to get module info
const yaml = require ( 'yaml' ) ;
const moduleYamlPath = path . join ( sourcePath , 'module.yaml' ) ;
const moduleContent = await fs . readFile ( moduleYamlPath , 'utf8' ) ;
const moduleData = yaml . parse ( moduleContent ) ;
// Add to sources
customContentConfig . sources . push ( {
path : sourcePath ,
id : moduleData . code ,
name : moduleData . name || moduleData . code ,
} ) ;
2025-12-15 11:33:01 +08:00
console . log ( chalk . green ( ` ✓ Confirmed local custom module: ${ moduleData . name || moduleData . code } ` ) ) ;
2025-12-14 10:03:25 +08:00
}
// Ask if user wants to add these to the installation
const { shouldInstall } = await inquirer . prompt ( [
{
type : 'confirm' ,
name : 'shouldInstall' ,
message : ` Install ${ customContentConfig . sources . length } custom module(s) now? ` ,
default : true ,
} ,
] ) ;
if ( shouldInstall ) {
customContentConfig . selected = true ;
// Store paths to module.yaml files, not directories
customContentConfig . selectedFiles = customContentConfig . sources . map ( ( s ) => path . join ( s . path , 'module.yaml' ) ) ;
// Also include module IDs for installation
customContentConfig . selectedModuleIds = customContentConfig . sources . map ( ( s ) => s . id ) ;
}
return customContentConfig ;
}
2025-12-15 23:53:26 +08:00
/ * *
* Handle custom modules in the modify flow
* @ param { string } directory - Installation directory
* @ param { Array } selectedModules - Currently selected modules
* @ returns { Object } Result with selected custom modules and custom content config
* /
async handleCustomModulesInModifyFlow ( directory , selectedModules ) {
// Get existing installation to find custom modules
const { existingInstall } = await this . getExistingInstallation ( directory ) ;
// Check if there are any custom modules in cache
const { Installer } = require ( '../installers/lib/core/installer' ) ;
const installer = new Installer ( ) ;
const { bmadDir } = await installer . findBmadDir ( directory ) ;
const cacheDir = path . join ( bmadDir , '_config' , 'custom' ) ;
const cachedCustomModules = [ ] ;
if ( await fs . pathExists ( cacheDir ) ) {
const entries = await fs . readdir ( cacheDir , { withFileTypes : true } ) ;
for ( const entry of entries ) {
if ( entry . isDirectory ( ) ) {
const moduleYamlPath = path . join ( cacheDir , entry . name , 'module.yaml' ) ;
if ( await fs . pathExists ( moduleYamlPath ) ) {
const yaml = require ( 'yaml' ) ;
const content = await fs . readFile ( moduleYamlPath , 'utf8' ) ;
const moduleData = yaml . parse ( content ) ;
cachedCustomModules . push ( {
id : entry . name ,
name : moduleData . name || entry . name ,
description : moduleData . description || 'Custom module from cache' ,
checked : selectedModules . includes ( entry . name ) ,
fromCache : true ,
} ) ;
}
}
}
}
const result = {
selectedCustomModules : [ ] ,
customContentConfig : { hasCustomContent : false } ,
} ;
// Ask user about custom modules
console . log ( chalk . cyan ( '\n⚙️ Custom Modules' ) ) ;
2025-12-16 20:33:10 +08:00
if ( cachedCustomModules . length > 0 ) {
console . log ( chalk . dim ( 'Found custom modules in your installation:' ) ) ;
} else {
console . log ( chalk . dim ( 'No custom modules currently installed.' ) ) ;
}
// Build choices dynamically based on whether we have existing modules
const choices = [ ] ;
if ( cachedCustomModules . length > 0 ) {
choices . push (
{ name : 'Keep all existing custom modules' , value : 'keep' } ,
{ name : 'Select which custom modules to keep' , value : 'select' } ,
{ name : 'Add new custom modules' , value : 'add' } ,
{ name : 'Remove all custom modules' , value : 'remove' } ,
) ;
} else {
choices . push ( { name : 'Add new custom modules' , value : 'add' } , { name : 'Cancel (no custom modules)' , value : 'cancel' } ) ;
}
2025-12-15 23:53:26 +08:00
const { customAction } = await inquirer . prompt ( [
{
type : 'list' ,
name : 'customAction' ,
2025-12-16 20:33:10 +08:00
message :
cachedCustomModules . length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?' ,
choices : choices ,
default : cachedCustomModules . length > 0 ? 'keep' : 'add' ,
2025-12-15 23:53:26 +08:00
} ,
] ) ;
switch ( customAction ) {
case 'keep' : {
// Keep all existing custom modules
result . selectedCustomModules = cachedCustomModules . map ( ( m ) => m . id ) ;
console . log ( chalk . dim ( ` Keeping ${ result . selectedCustomModules . length } custom module(s) ` ) ) ;
break ;
}
case 'select' : {
// Let user choose which to keep
const choices = cachedCustomModules . map ( ( m ) => ( {
name : ` ${ m . name } ${ chalk . gray ( ` ( ${ m . id } ) ` ) } ` ,
value : m . id ,
} ) ) ;
const { keepModules } = await inquirer . prompt ( [
{
type : 'checkbox' ,
name : 'keepModules' ,
message : 'Select custom modules to keep:' ,
choices : choices ,
default : cachedCustomModules . filter ( ( m ) => m . checked ) . map ( ( m ) => m . id ) ,
} ,
] ) ;
result . selectedCustomModules = keepModules ;
break ;
}
case 'add' : {
2025-12-16 20:33:10 +08:00
// By default, keep existing modules when adding new ones
// User chose "Add new" not "Replace", so we assume they want to keep existing
result . selectedCustomModules = cachedCustomModules . map ( ( m ) => m . id ) ;
2025-12-15 23:53:26 +08:00
// Then prompt for new ones (reuse existing method)
const newCustomContent = await this . promptCustomContentSource ( ) ;
if ( newCustomContent . hasCustomContent && newCustomContent . selected ) {
result . selectedCustomModules . push ( ... newCustomContent . selectedModuleIds ) ;
result . customContentConfig = newCustomContent ;
}
break ;
}
case 'remove' : {
// Remove all custom modules
console . log ( chalk . yellow ( 'All custom modules will be removed from the installation' ) ) ;
break ;
}
2025-12-16 20:33:10 +08:00
case 'cancel' : {
// User cancelled - no custom modules
console . log ( chalk . dim ( 'No custom modules will be added' ) ) ;
break ;
}
2025-12-15 23:53:26 +08:00
}
return result ;
}
2025-12-23 20:04:42 +08:00
/ * *
* Parse alpha version string ( e . g . , "6.0.0-Alpha.20" )
* @ param { string } version - Version string
* @ returns { Object | null } Object with alphaNumber and fullVersion , or null if invalid
* /
parseAlphaVersion ( version ) {
if ( ! version || version === 'unknown' ) {
return null ;
}
// Remove 'v' prefix if present
const cleanVersion = version . toString ( ) . replace ( /^v/i , '' ) ;
// Match alpha version pattern: X.Y.Z-Alpha.N (case-insensitive)
const match = cleanVersion . match ( /[\d.]+-Alpha\.(\d+)/i ) ;
if ( ! match ) {
return null ;
}
return {
alphaNumber : parseInt ( match [ 1 ] , 10 ) ,
fullVersion : cleanVersion ,
} ;
}
/ * *
* Check if installed version is more than 2 alpha versions behind current
* @ param { string } installedVersion - The installed version
* @ param { string } currentVersion - The current version
* @ returns { Object } Object with { isOldVersion , versionDiff , shouldWarn , installed , current }
* /
checkAlphaVersionAge ( installedVersion , currentVersion ) {
const installed = this . parseAlphaVersion ( installedVersion ) ;
const current = this . parseAlphaVersion ( currentVersion ) ;
// If we can't parse either version, don't warn
if ( ! installed || ! current ) {
return { isOldVersion : false , versionDiff : 0 , shouldWarn : false } ;
}
// Calculate alpha version difference
const versionDiff = current . alphaNumber - installed . alphaNumber ;
// Consider it old if more than 2 versions behind
const isOldVersion = versionDiff > 2 ;
return {
isOldVersion ,
versionDiff ,
shouldWarn : isOldVersion ,
installed : installed . fullVersion ,
current : current . fullVersion ,
installedAlpha : installed . alphaNumber ,
currentAlpha : current . alphaNumber ,
} ;
}
/ * *
* Show warning for old alpha version and ask if user wants to proceed
* @ param { string } installedVersion - The installed version
* @ param { string } currentVersion - The current version
* @ param { string } bmadFolderName - Name of the BMAD folder
* @ returns { Promise < boolean > } True if user wants to proceed , false if they cancel
* /
async showOldAlphaVersionWarning ( installedVersion , currentVersion , bmadFolderName ) {
const versionInfo = this . checkAlphaVersionAge ( installedVersion , currentVersion ) ;
// Also warn if version is unknown or can't be parsed (legacy/unsupported)
const isUnknownVersion = installedVersion === 'unknown' || ! versionInfo . installed ;
if ( ! versionInfo . shouldWarn && ! isUnknownVersion ) {
return true ; // Not old, proceed
}
console . log ( '' ) ;
console . log ( chalk . yellow . bold ( '⚠️ VERSION WARNING' ) ) ;
console . log ( chalk . yellow ( '─' . repeat ( 80 ) ) ) ;
if ( isUnknownVersion ) {
console . log ( chalk . yellow ( 'Unable to detect your installed BMAD version.' ) ) ;
console . log ( chalk . yellow ( 'This appears to be a legacy or unsupported installation.' ) ) ;
console . log ( '' ) ;
console . log ( chalk . dim ( 'For stability, we only support updates from the previous 2 alpha versions.' ) ) ;
console . log ( chalk . dim ( 'Legacy installations may have compatibility issues.' ) ) ;
} else {
console . log ( chalk . yellow ( ` You are updating from ${ versionInfo . installed } to ${ versionInfo . current } . ` ) ) ;
console . log ( chalk . yellow ( ` This is ${ versionInfo . versionDiff } alpha versions behind. ` ) ) ;
console . log ( '' ) ;
console . log ( chalk . dim ( ` For stability, we only support updates from the previous 2 alpha versions ` ) ) ;
console . log ( chalk . dim ( ` (Alpha. ${ versionInfo . currentAlpha - 2 } through Alpha. ${ versionInfo . currentAlpha - 1 } ). ` ) ) ;
}
console . log ( '' ) ;
console . log ( chalk . dim ( 'For the best experience, we recommend:' ) ) ;
console . log ( chalk . dim ( ' 1. Delete your current BMAD installation folder' ) ) ;
console . log ( chalk . dim ( ` (the " ${ bmadFolderName } /" folder in your project) ` ) ) ;
console . log ( chalk . dim ( ' 2. Run a fresh installation' ) ) ;
console . log ( '' ) ;
console . log ( chalk . dim ( 'Benefits of a fresh install:' ) ) ;
console . log ( chalk . dim ( ' • Cleaner configuration without legacy artifacts' ) ) ;
console . log ( chalk . dim ( ' • All new features properly configured' ) ) ;
console . log ( chalk . dim ( ' • Fewer potential conflicts' ) ) ;
console . log ( chalk . yellow ( '─' . repeat ( 80 ) ) ) ;
console . log ( '' ) ;
const { proceed } = await inquirer . prompt ( [
{
type : 'list' ,
name : 'proceed' ,
message : 'What would you like to do?' ,
choices : [
{
name : 'Proceed with update anyway (may have issues)' ,
value : 'proceed' ,
short : 'Proceed with update' ,
} ,
{
name : 'Cancel (recommended - do a fresh install instead)' ,
value : 'cancel' ,
short : 'Cancel installation' ,
} ,
] ,
default : 'cancel' ,
} ,
] ) ;
if ( proceed === 'cancel' ) {
console . log ( '' ) ;
console . log ( chalk . cyan ( 'To do a fresh install:' ) ) ;
console . log ( chalk . dim ( ` 1. Delete the " ${ bmadFolderName } /" folder in your project ` ) ) ;
console . log ( chalk . dim ( " 2. Run 'bmad install' again" ) ) ;
console . log ( '' ) ;
}
return proceed === 'proceed' ;
}
2025-09-28 23:17:07 -05:00
}
module . exports = { UI } ;