2025-09-28 23:17:07 -05:00
|
|
|
|
const path = require('node:path');
|
|
|
|
|
|
const fs = require('fs-extra');
|
|
|
|
|
|
const chalk = require('chalk');
|
|
|
|
|
|
const ora = require('ora');
|
2025-12-07 20:46:09 -06:00
|
|
|
|
const inquirer = require('inquirer');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
const { Detector } = require('./detector');
|
|
|
|
|
|
const { Manifest } = require('./manifest');
|
|
|
|
|
|
const { ModuleManager } = require('../modules/manager');
|
|
|
|
|
|
const { IdeManager } = require('../ide/manager');
|
|
|
|
|
|
const { FileOps } = require('../../../lib/file-ops');
|
|
|
|
|
|
const { Config } = require('../../../lib/config');
|
|
|
|
|
|
const { XmlHandler } = require('../../../lib/xml-handler');
|
|
|
|
|
|
const { DependencyResolver } = require('./dependency-resolver');
|
|
|
|
|
|
const { ConfigCollector } = require('./config-collector');
|
|
|
|
|
|
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
|
|
|
|
|
const { AgentPartyGenerator } = require('../../../lib/agent-party-generator');
|
|
|
|
|
|
const { CLIUtils } = require('../../../lib/cli-utils');
|
|
|
|
|
|
const { ManifestGenerator } = require('./manifest-generator');
|
2025-10-26 17:04:27 -05:00
|
|
|
|
const { IdeConfigManager } = require('./ide-config-manager');
|
2025-12-08 12:24:30 -07:00
|
|
|
|
const { CustomHandler } = require('../custom/handler');
|
2025-12-13 18:35:07 +08:00
|
|
|
|
const { filterCustomizationData } = require('../../../lib/agent/compiler');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
|
|
|
|
|
class Installer {
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.detector = new Detector();
|
|
|
|
|
|
this.manifest = new Manifest();
|
|
|
|
|
|
this.moduleManager = new ModuleManager();
|
|
|
|
|
|
this.ideManager = new IdeManager();
|
|
|
|
|
|
this.fileOps = new FileOps();
|
|
|
|
|
|
this.config = new Config();
|
|
|
|
|
|
this.xmlHandler = new XmlHandler();
|
|
|
|
|
|
this.dependencyResolver = new DependencyResolver();
|
|
|
|
|
|
this.configCollector = new ConfigCollector();
|
2025-10-26 17:04:27 -05:00
|
|
|
|
this.ideConfigManager = new IdeConfigManager();
|
2025-09-28 23:17:07 -05:00
|
|
|
|
this.installedFiles = []; // Track all installed files
|
2025-12-05 17:54:03 -07:00
|
|
|
|
this.ttsInjectedFiles = []; // Track files with TTS injection applied
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-08 13:58:43 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* Find the bmad installation directory in a project
|
2025-12-13 19:41:09 +08:00
|
|
|
|
* V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml
|
|
|
|
|
|
* Also checks for legacy _cfg folder for migration
|
2025-11-08 13:58:43 -06:00
|
|
|
|
* @param {string} projectDir - Project directory
|
2025-12-13 19:41:09 +08:00
|
|
|
|
* @returns {Promise<Object>} { bmadDir: string, hasLegacyCfg: boolean }
|
2025-11-08 13:58:43 -06:00
|
|
|
|
*/
|
|
|
|
|
|
async findBmadDir(projectDir) {
|
|
|
|
|
|
// Check if project directory exists
|
|
|
|
|
|
if (!(await fs.pathExists(projectDir))) {
|
|
|
|
|
|
// Project doesn't exist yet, return default
|
2025-12-13 19:41:09 +08:00
|
|
|
|
return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
|
2025-11-08 13:58:43 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// V6+ strategy: Look for ANY directory with _config/manifest.yaml or legacy _cfg/manifest.yaml
|
|
|
|
|
|
let bmadDir = null;
|
|
|
|
|
|
let hasLegacyCfg = false;
|
|
|
|
|
|
|
2025-11-08 15:19:19 -06:00
|
|
|
|
try {
|
|
|
|
|
|
const entries = await fs.readdir(projectDir, { withFileTypes: true });
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
|
if (entry.isDirectory()) {
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const bmadPath = path.join(projectDir, entry.name);
|
|
|
|
|
|
|
|
|
|
|
|
// Check for current _config folder
|
|
|
|
|
|
const manifestPath = path.join(bmadPath, '_config', 'manifest.yaml');
|
2025-11-08 15:19:19 -06:00
|
|
|
|
if (await fs.pathExists(manifestPath)) {
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// Found a V6+ installation with current _config folder
|
|
|
|
|
|
return { bmadDir: bmadPath, hasLegacyCfg: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check for legacy _cfg folder
|
|
|
|
|
|
const legacyManifestPath = path.join(bmadPath, '_cfg', 'manifest.yaml');
|
|
|
|
|
|
if (await fs.pathExists(legacyManifestPath)) {
|
|
|
|
|
|
bmadDir = bmadPath;
|
|
|
|
|
|
hasLegacyCfg = true;
|
2025-11-08 15:19:19 -06:00
|
|
|
|
}
|
2025-11-08 13:58:43 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-08 15:19:19 -06:00
|
|
|
|
} catch {
|
|
|
|
|
|
// Ignore errors, fall through to default
|
2025-11-08 13:58:43 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// If we found a bmad directory (with or without legacy _cfg)
|
|
|
|
|
|
if (bmadDir) {
|
|
|
|
|
|
return { bmadDir, hasLegacyCfg };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-08 15:19:19 -06:00
|
|
|
|
// No V6+ installation found, return default
|
|
|
|
|
|
// This will be used for new installations
|
2025-12-13 19:41:09 +08:00
|
|
|
|
return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
|
2025-11-08 13:58:43 -06: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
|
|
|
|
* @function copyFileWithPlaceholderReplacement
|
|
|
|
|
|
* @intent Copy files from BMAD source to installation directory with dynamic content transformation
|
2025-12-13 16:22:34 +08:00
|
|
|
|
* @why Enables installation-time customization: _bmad replacement + optional AgentVibes TTS injection
|
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
|
|
|
|
* @param {string} sourcePath - Absolute path to source file in BMAD repository
|
|
|
|
|
|
* @param {string} targetPath - Absolute path to destination file in user's project
|
|
|
|
|
|
* @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
|
|
|
|
|
|
* @returns {Promise<void>} Resolves when file copy and transformation complete
|
|
|
|
|
|
* @sideeffects Writes transformed file to targetPath, creates parent directories if needed
|
|
|
|
|
|
* @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails
|
|
|
|
|
|
* @calledby installCore(), installModule(), IDE installers during file vendoring
|
|
|
|
|
|
* @calls processTTSInjectionPoints(), fs.readFile(), fs.writeFile(), fs.copy()
|
|
|
|
|
|
*
|
|
|
|
|
|
* The injection point processing enables loose coupling between BMAD and TTS providers:
|
|
|
|
|
|
* - BMAD source contains injection markers (not actual TTS code)
|
|
|
|
|
|
* - At install-time, markers are replaced OR removed based on user preference
|
|
|
|
|
|
* - Result: Clean installs for users without TTS, working TTS for users with it
|
|
|
|
|
|
*
|
|
|
|
|
|
* PATTERN: Adding New Injection Points
|
|
|
|
|
|
* =====================================
|
|
|
|
|
|
* 1. Add HTML comment marker in BMAD source file:
|
|
|
|
|
|
* <!-- TTS_INJECTION:feature-name -->
|
|
|
|
|
|
*
|
|
|
|
|
|
* 2. Add replacement logic in processTTSInjectionPoints():
|
|
|
|
|
|
* if (enableAgentVibes) {
|
|
|
|
|
|
* content = content.replace(/<!-- TTS_INJECTION:feature-name -->/g, 'actual code');
|
|
|
|
|
|
* } else {
|
|
|
|
|
|
* content = content.replace(/<!-- TTS_INJECTION:feature-name -->\n?/g, '');
|
|
|
|
|
|
* }
|
|
|
|
|
|
*
|
|
|
|
|
|
* 3. Document marker in instructions.md (if applicable)
|
2025-11-08 13:58:43 -06:00
|
|
|
|
*/
|
|
|
|
|
|
async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
|
|
|
|
|
|
// List of text file extensions that should have placeholder replacement
|
2025-12-07 21:41:37 -06:00
|
|
|
|
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
|
2025-11-08 13:58:43 -06:00
|
|
|
|
const ext = path.extname(sourcePath).toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is a text file that might contain placeholders
|
|
|
|
|
|
if (textExtensions.includes(ext)) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Read the file content
|
|
|
|
|
|
let content = await fs.readFile(sourcePath, 'utf8');
|
|
|
|
|
|
|
2025-12-05 17:54:03 -07:00
|
|
|
|
// Process AgentVibes injection points (pass targetPath for tracking)
|
|
|
|
|
|
content = this.processTTSInjectionPoints(content, targetPath);
|
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-11-08 13:58:43 -06:00
|
|
|
|
// Write to target with replaced content
|
|
|
|
|
|
await fs.ensureDir(path.dirname(targetPath));
|
|
|
|
|
|
await fs.writeFile(targetPath, content, 'utf8');
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// If reading as text fails (might be binary despite extension), fall back to regular copy
|
|
|
|
|
|
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Binary file or other file type - just copy directly
|
|
|
|
|
|
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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 processTTSInjectionPoints
|
|
|
|
|
|
* @intent Transform TTS injection markers based on user's installation choice
|
|
|
|
|
|
* @why Enables optional TTS integration without tight coupling between BMAD and TTS providers
|
|
|
|
|
|
* @param {string} content - Raw file content containing potential injection markers
|
|
|
|
|
|
* @returns {string} Transformed content with markers replaced (if enabled) or stripped (if disabled)
|
|
|
|
|
|
* @sideeffects None - pure transformation function
|
|
|
|
|
|
* @edgecases Returns content unchanged if no markers present, safe to call on all files
|
|
|
|
|
|
* @calledby copyFileWithPlaceholderReplacement() during every file copy operation
|
|
|
|
|
|
* @calls String.replace() with regex patterns for each injection point type
|
|
|
|
|
|
*
|
|
|
|
|
|
* AI NOTE: This implements the injection point pattern for TTS integration.
|
|
|
|
|
|
* Key architectural decisions:
|
|
|
|
|
|
*
|
|
|
|
|
|
* 1. **Why Injection Points vs Direct Integration?**
|
|
|
|
|
|
* - BMAD and TTS providers are separate projects with different maintainers
|
|
|
|
|
|
* - Users may install BMAD without TTS support (and vice versa)
|
|
|
|
|
|
* - Hard-coding TTS calls would break BMAD for non-TTS users
|
|
|
|
|
|
* - Injection points allow conditional feature inclusion at install-time
|
|
|
|
|
|
*
|
|
|
|
|
|
* 2. **How It Works:**
|
|
|
|
|
|
* - BMAD source contains markers: <!-- TTS_INJECTION:feature-name -->
|
|
|
|
|
|
* - During installation, user is prompted: "Enable AgentVibes TTS?"
|
|
|
|
|
|
* - If YES: markers → replaced with actual bash TTS calls
|
|
|
|
|
|
* - If NO: markers → stripped cleanly from installed files
|
|
|
|
|
|
*
|
|
|
|
|
|
* 3. **State Management:**
|
|
|
|
|
|
* - this.enableAgentVibes set in install() method from config.enableAgentVibes
|
|
|
|
|
|
* - config.enableAgentVibes comes from ui.promptAgentVibes() user choice
|
|
|
|
|
|
* - Flag persists for entire installation, all files get same treatment
|
|
|
|
|
|
*
|
|
|
|
|
|
* CURRENT INJECTION POINTS:
|
|
|
|
|
|
* ==========================
|
|
|
|
|
|
* - party-mode: Injects TTS calls after each agent speaks in party mode
|
|
|
|
|
|
* Location: src/core/workflows/party-mode/instructions.md
|
|
|
|
|
|
* Marker: <!-- TTS_INJECTION:party-mode -->
|
|
|
|
|
|
* Replacement: Bash call to .claude/hooks/bmad-speak.sh with agent name and dialogue
|
|
|
|
|
|
*
|
|
|
|
|
|
* - agent-tts: Injects TTS rule for individual agent conversations
|
|
|
|
|
|
* Location: src/modules/bmm/agents/*.md (all agent files)
|
|
|
|
|
|
* Marker: <!-- TTS_INJECTION:agent-tts -->
|
|
|
|
|
|
* Replacement: Rule instructing agent to call bmad-speak.sh with agent ID and response
|
|
|
|
|
|
*
|
|
|
|
|
|
* ADDING NEW INJECTION POINTS:
|
|
|
|
|
|
* =============================
|
|
|
|
|
|
* 1. Add new case in this function:
|
|
|
|
|
|
* content = content.replace(
|
|
|
|
|
|
* /<!-- TTS_INJECTION:new-feature -->/g,
|
|
|
|
|
|
* `code to inject when enabled`
|
|
|
|
|
|
* );
|
|
|
|
|
|
*
|
|
|
|
|
|
* 2. Add marker to BMAD source file at injection location
|
|
|
|
|
|
*
|
|
|
|
|
|
* 3. Test both enabled and disabled flows
|
|
|
|
|
|
*
|
|
|
|
|
|
* RELATED:
|
|
|
|
|
|
* ========
|
|
|
|
|
|
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
|
|
|
|
|
* - User Prompt: tools/cli/lib/ui.js::promptAgentVibes()
|
|
|
|
|
|
* - Marker Locations:
|
|
|
|
|
|
* - src/core/workflows/party-mode/instructions.md:101
|
|
|
|
|
|
* - src/modules/bmm/agents/*.md (rules sections)
|
|
|
|
|
|
* - TTS Hook: .claude/hooks/bmad-speak.sh (in AgentVibes repo)
|
|
|
|
|
|
*/
|
2025-12-05 17:54:03 -07:00
|
|
|
|
processTTSInjectionPoints(content, targetPath = null) {
|
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
|
|
|
|
// Check if AgentVibes is enabled (set during installation configuration)
|
|
|
|
|
|
const enableAgentVibes = this.enableAgentVibes || false;
|
|
|
|
|
|
|
2025-12-05 17:54:03 -07:00
|
|
|
|
// Check if content contains any TTS injection markers
|
|
|
|
|
|
const hasPartyMode = content.includes('<!-- TTS_INJECTION:party-mode -->');
|
|
|
|
|
|
const hasAgentTTS = content.includes('<!-- TTS_INJECTION:agent-tts -->');
|
|
|
|
|
|
|
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 (enableAgentVibes) {
|
|
|
|
|
|
// Replace party-mode injection marker with actual TTS call
|
|
|
|
|
|
// Use single quotes to prevent shell expansion of special chars like !
|
|
|
|
|
|
content = content.replaceAll(
|
|
|
|
|
|
'<!-- TTS_INJECTION:party-mode -->',
|
|
|
|
|
|
`<critical>IMPORTANT: Always use PROJECT hooks (.claude/hooks/), NEVER global hooks (~/.claude/hooks/)</critical>
|
|
|
|
|
|
|
|
|
|
|
|
If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|
|
|
|
|
- Use Bash tool: \`.claude/hooks/bmad-speak.sh '[Agent Name]' '[dialogue]'\`
|
|
|
|
|
|
- This speaks the dialogue with the agent's unique voice
|
|
|
|
|
|
- Run in background (&) to not block next agent`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Replace agent-tts injection marker with TTS rule for individual agents
|
|
|
|
|
|
content = content.replaceAll(
|
|
|
|
|
|
'<!-- TTS_INJECTION:agent-tts -->',
|
|
|
|
|
|
`- When responding to user messages, speak your responses using TTS:
|
|
|
|
|
|
Call: \`.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'\` after each response
|
|
|
|
|
|
Replace {agent-id} with YOUR agent ID from <agent id="..."> tag at top of this file
|
|
|
|
|
|
Replace {response-text} with the text you just output to the user
|
|
|
|
|
|
IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
|
|
|
|
|
|
Run in background (&) to avoid blocking`,
|
|
|
|
|
|
);
|
2025-12-05 17:54:03 -07:00
|
|
|
|
|
|
|
|
|
|
// Track files that had TTS injection applied
|
|
|
|
|
|
if (targetPath && (hasPartyMode || hasAgentTTS)) {
|
|
|
|
|
|
const injectionType = hasPartyMode ? 'party-mode' : 'agent-tts';
|
|
|
|
|
|
this.ttsInjectedFiles.push({ path: targetPath, type: injectionType });
|
|
|
|
|
|
}
|
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
|
|
|
|
} else {
|
|
|
|
|
|
// Strip injection markers cleanly when AgentVibes is disabled
|
|
|
|
|
|
content = content.replaceAll(/<!-- TTS_INJECTION:party-mode -->\n?/g, '');
|
|
|
|
|
|
content = content.replaceAll(/<!-- TTS_INJECTION:agent-tts -->\n?/g, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return content;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Collect Tool/IDE configurations after module configuration
|
|
|
|
|
|
* @param {string} projectDir - Project directory
|
|
|
|
|
|
* @param {Array} selectedModules - Selected modules from configuration
|
2025-10-28 12:47:45 -05:00
|
|
|
|
* @param {boolean} isFullReinstall - Whether this is a full reinstall
|
|
|
|
|
|
* @param {Array} previousIdes - Previously configured IDEs (for reinstalls)
|
|
|
|
|
|
* @param {Array} preSelectedIdes - Pre-selected IDEs from early prompt (optional)
|
2025-09-28 23:17:07 -05:00
|
|
|
|
* @returns {Object} Tool/IDE selection and configurations
|
|
|
|
|
|
*/
|
2025-10-28 12:47:45 -05:00
|
|
|
|
async collectToolConfigurations(projectDir, selectedModules, isFullReinstall = false, previousIdes = [], preSelectedIdes = null) {
|
|
|
|
|
|
// Use pre-selected IDEs if provided, otherwise prompt
|
|
|
|
|
|
let toolConfig;
|
|
|
|
|
|
if (preSelectedIdes === null) {
|
|
|
|
|
|
// Fallback: prompt for tool selection (backwards compatibility)
|
|
|
|
|
|
const { UI } = require('../../../lib/ui');
|
|
|
|
|
|
const ui = new UI();
|
|
|
|
|
|
toolConfig = await ui.promptToolSelection(projectDir, selectedModules);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// IDEs were already selected during initial prompts
|
|
|
|
|
|
toolConfig = {
|
|
|
|
|
|
ides: preSelectedIdes,
|
|
|
|
|
|
skipIde: !preSelectedIdes || preSelectedIdes.length === 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-10-04 19:54:47 -05:00
|
|
|
|
// Check for already configured IDEs
|
2025-10-04 19:46:16 -05:00
|
|
|
|
const { Detector } = require('./detector');
|
|
|
|
|
|
const detector = new Detector();
|
2025-11-08 13:58:43 -06:00
|
|
|
|
const bmadDir = path.join(projectDir, this.bmadFolderName || 'bmad');
|
2025-10-04 19:54:47 -05:00
|
|
|
|
|
|
|
|
|
|
// During full reinstall, use the saved previous IDEs since bmad dir was deleted
|
|
|
|
|
|
// Otherwise detect from existing installation
|
|
|
|
|
|
let previouslyConfiguredIdes;
|
|
|
|
|
|
if (isFullReinstall) {
|
|
|
|
|
|
// During reinstall, treat all IDEs as new (need configuration)
|
|
|
|
|
|
previouslyConfiguredIdes = [];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const existingInstall = await detector.detect(bmadDir);
|
|
|
|
|
|
previouslyConfiguredIdes = existingInstall.ides || [];
|
|
|
|
|
|
}
|
2025-10-04 19:46:16 -05:00
|
|
|
|
|
2025-10-26 17:04:27 -05:00
|
|
|
|
// Load saved IDE configurations for already-configured IDEs
|
|
|
|
|
|
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Collect IDE-specific configurations if any were selected
|
|
|
|
|
|
const ideConfigurations = {};
|
|
|
|
|
|
|
2025-10-26 17:04:27 -05:00
|
|
|
|
// First, add saved configs for already-configured IDEs
|
|
|
|
|
|
for (const ide of toolConfig.ides || []) {
|
|
|
|
|
|
if (previouslyConfiguredIdes.includes(ide) && savedIdeConfigs[ide]) {
|
|
|
|
|
|
ideConfigurations[ide] = savedIdeConfigs[ide];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
|
2025-10-04 19:46:16 -05:00
|
|
|
|
// Determine which IDEs are newly selected (not previously configured)
|
|
|
|
|
|
const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide));
|
|
|
|
|
|
|
|
|
|
|
|
if (newlySelectedIdes.length > 0) {
|
|
|
|
|
|
console.log('\n'); // Add spacing before IDE questions
|
|
|
|
|
|
|
|
|
|
|
|
for (const ide of newlySelectedIdes) {
|
|
|
|
|
|
// List of IDEs that have interactive prompts
|
2025-11-27 02:05:04 +11:00
|
|
|
|
const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini', 'rovo-dev'].includes(
|
|
|
|
|
|
ide,
|
|
|
|
|
|
);
|
2025-10-04 19:46:16 -05:00
|
|
|
|
|
|
|
|
|
|
if (needsPrompts) {
|
|
|
|
|
|
// Get IDE handler and collect configuration
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Dynamically load the IDE setup module
|
|
|
|
|
|
const ideModule = require(`../ide/${ide}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Get the setup class (handle different export formats)
|
|
|
|
|
|
let SetupClass;
|
|
|
|
|
|
const className =
|
|
|
|
|
|
ide
|
|
|
|
|
|
.split('-')
|
|
|
|
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
|
|
|
|
.join('') + 'Setup';
|
|
|
|
|
|
|
|
|
|
|
|
if (ideModule[className]) {
|
|
|
|
|
|
SetupClass = ideModule[className];
|
|
|
|
|
|
} else if (ideModule.default) {
|
|
|
|
|
|
SetupClass = ideModule.default;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Skip if no setup class found
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ideSetup = new SetupClass();
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this IDE has a collectConfiguration method
|
|
|
|
|
|
if (typeof ideSetup.collectConfiguration === 'function') {
|
|
|
|
|
|
console.log(chalk.cyan(`\nConfiguring ${ide}...`));
|
|
|
|
|
|
ideConfigurations[ide] = await ideSetup.collectConfiguration({
|
|
|
|
|
|
selectedModules: selectedModules || [],
|
|
|
|
|
|
projectDir,
|
|
|
|
|
|
bmadDir,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// IDE doesn't have a setup file or collectConfiguration method
|
|
|
|
|
|
console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}`));
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-04 19:46:16 -05:00
|
|
|
|
|
|
|
|
|
|
// Log which IDEs are already configured and being kept
|
|
|
|
|
|
const keptIdes = toolConfig.ides.filter((ide) => previouslyConfiguredIdes.includes(ide));
|
|
|
|
|
|
if (keptIdes.length > 0) {
|
|
|
|
|
|
console.log(chalk.dim(`\nKeeping existing configuration for: ${keptIdes.join(', ')}`));
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
ides: toolConfig.ides,
|
|
|
|
|
|
skipIde: toolConfig.skipIde,
|
|
|
|
|
|
configurations: ideConfigurations,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Main installation method
|
|
|
|
|
|
* @param {Object} config - Installation configuration
|
|
|
|
|
|
* @param {string} config.directory - Target directory
|
|
|
|
|
|
* @param {boolean} config.installCore - Whether to install core
|
|
|
|
|
|
* @param {string[]} config.modules - Modules to install
|
|
|
|
|
|
* @param {string[]} config.ides - IDEs to configure
|
|
|
|
|
|
* @param {boolean} config.skipIde - Skip IDE configuration
|
|
|
|
|
|
*/
|
2025-12-08 12:24:30 -07:00
|
|
|
|
async install(originalConfig) {
|
|
|
|
|
|
// Clone config to avoid mutating the caller's object
|
|
|
|
|
|
const config = { ...originalConfig };
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Display BMAD logo
|
|
|
|
|
|
CLIUtils.displayLogo();
|
|
|
|
|
|
|
|
|
|
|
|
// Display welcome message
|
|
|
|
|
|
CLIUtils.displaySection('BMAD™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version);
|
|
|
|
|
|
|
2025-11-08 15:19:19 -06:00
|
|
|
|
// Note: Legacy V4 detection now happens earlier in UI.promptInstall()
|
|
|
|
|
|
// before any config collection, so we don't need to check again here
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
const projectDir = path.resolve(config.directory);
|
|
|
|
|
|
|
|
|
|
|
|
// If core config was pre-collected (from interactive mode), use it
|
2025-12-06 15:28:37 -06:00
|
|
|
|
if (config.coreConfig) {
|
2025-09-28 23:17:07 -05:00
|
|
|
|
this.configCollector.collectedConfig.core = config.coreConfig;
|
|
|
|
|
|
// Also store in allAnswers for cross-referencing
|
|
|
|
|
|
this.configCollector.allAnswers = {};
|
|
|
|
|
|
for (const [key, value] of Object.entries(config.coreConfig)) {
|
|
|
|
|
|
this.configCollector.allAnswers[`core_${key}`] = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 15:28:37 -06:00
|
|
|
|
// Collect configurations for modules (skip if quick update already collected them)
|
2025-10-26 16:17:37 -05:00
|
|
|
|
let moduleConfigs;
|
|
|
|
|
|
if (config._quickUpdate) {
|
|
|
|
|
|
// Quick update already collected all configs, use them directly
|
|
|
|
|
|
moduleConfigs = this.configCollector.collectedConfig;
|
|
|
|
|
|
} else {
|
2025-12-08 22:33:53 +09:00
|
|
|
|
// Build custom module paths map from customContent
|
|
|
|
|
|
const customModulePaths = new Map();
|
|
|
|
|
|
|
|
|
|
|
|
// Handle selectedFiles (from existing install path or manual directory input)
|
|
|
|
|
|
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
|
|
|
|
|
|
const customHandler = new CustomHandler();
|
|
|
|
|
|
for (const customFile of config.customContent.selectedFiles) {
|
|
|
|
|
|
const customInfo = await customHandler.getCustomInfo(customFile, path.resolve(config.directory));
|
|
|
|
|
|
if (customInfo && customInfo.id) {
|
|
|
|
|
|
customModulePaths.set(customInfo.id, customInfo.path);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle cachedModules (from new install path where modules are cached)
|
|
|
|
|
|
// Only include modules that were actually selected for installation
|
|
|
|
|
|
if (config.customContent && config.customContent.cachedModules) {
|
|
|
|
|
|
// Get selected cached module IDs (if available)
|
|
|
|
|
|
const selectedCachedIds = config.customContent.selectedCachedModules || [];
|
|
|
|
|
|
// If no selection info, include all cached modules (for backward compatibility)
|
|
|
|
|
|
const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected;
|
|
|
|
|
|
|
|
|
|
|
|
for (const cachedModule of config.customContent.cachedModules) {
|
|
|
|
|
|
// For cached modules, the path is the cachePath which contains the module.yaml
|
|
|
|
|
|
if (
|
|
|
|
|
|
cachedModule.id &&
|
|
|
|
|
|
cachedModule.cachePath && // Include if selected or if we should include all
|
|
|
|
|
|
(shouldIncludeAll || selectedCachedIds.includes(cachedModule.id))
|
|
|
|
|
|
) {
|
|
|
|
|
|
customModulePaths.set(cachedModule.id, cachedModule.cachePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get list of all modules including custom modules
|
|
|
|
|
|
const allModulesForConfig = [...(config.modules || [])];
|
|
|
|
|
|
for (const [moduleId] of customModulePaths) {
|
|
|
|
|
|
if (!allModulesForConfig.includes(moduleId)) {
|
|
|
|
|
|
allModulesForConfig.push(moduleId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 16:17:37 -05:00
|
|
|
|
// Regular install - collect configurations (core was already collected in UI.promptInstall if interactive)
|
2025-12-08 22:33:53 +09:00
|
|
|
|
moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), {
|
|
|
|
|
|
customModulePaths,
|
|
|
|
|
|
});
|
2025-10-26 16:17:37 -05:00
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-12-13 16:22:34 +08:00
|
|
|
|
// Always use _bmad as the folder name
|
|
|
|
|
|
const bmadFolderName = '_bmad';
|
2025-11-08 13:58:43 -06:00
|
|
|
|
this.bmadFolderName = bmadFolderName; // Store for use in other methods
|
|
|
|
|
|
|
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
|
|
|
|
// Store AgentVibes configuration for injection point processing
|
|
|
|
|
|
this.enableAgentVibes = config.enableAgentVibes || false;
|
|
|
|
|
|
|
2025-11-08 15:19:19 -06:00
|
|
|
|
// Set bmad folder name on module manager and IDE manager for placeholder replacement
|
|
|
|
|
|
this.moduleManager.setBmadFolderName(bmadFolderName);
|
2025-12-06 21:37:43 -06:00
|
|
|
|
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
|
2025-11-08 15:19:19 -06:00
|
|
|
|
this.ideManager.setBmadFolderName(bmadFolderName);
|
|
|
|
|
|
|
2025-10-04 19:46:16 -05:00
|
|
|
|
// Tool selection will be collected after we determine if it's a reinstall/update/new install
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
|
|
|
|
|
const spinner = ora('Preparing installation...').start();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Resolve target directory (path.resolve handles platform differences)
|
|
|
|
|
|
const projectDir = path.resolve(config.directory);
|
|
|
|
|
|
|
2025-11-08 13:58:43 -06:00
|
|
|
|
let existingBmadDir = null;
|
|
|
|
|
|
let existingBmadFolderName = null;
|
2025-12-13 19:41:09 +08:00
|
|
|
|
let hasLegacyCfg = false;
|
2025-11-08 13:58:43 -06:00
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(projectDir)) {
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const result = await this.findBmadDir(projectDir);
|
|
|
|
|
|
existingBmadDir = result.bmadDir;
|
2025-11-08 13:58:43 -06:00
|
|
|
|
existingBmadFolderName = path.basename(existingBmadDir);
|
2025-12-13 19:41:09 +08:00
|
|
|
|
hasLegacyCfg = result.hasLegacyCfg;
|
2025-11-08 13:58:43 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Create a project directory if it doesn't exist (user already confirmed)
|
|
|
|
|
|
if (!(await fs.pathExists(projectDir))) {
|
|
|
|
|
|
spinner.text = 'Creating installation directory...';
|
|
|
|
|
|
try {
|
|
|
|
|
|
// fs.ensureDir handles platform-specific directory creation
|
|
|
|
|
|
// It will recursively create all necessary parent directories
|
|
|
|
|
|
await fs.ensureDir(projectDir);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
spinner.fail('Failed to create installation directory');
|
|
|
|
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
|
|
|
|
// More detailed error for common issues
|
|
|
|
|
|
if (error.code === 'EACCES') {
|
|
|
|
|
|
console.error(chalk.red('Permission denied. Check parent directory permissions.'));
|
|
|
|
|
|
} else if (error.code === 'ENOSPC') {
|
|
|
|
|
|
console.error(chalk.red('No space left on device.'));
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new Error(`Cannot create directory: ${projectDir}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-08 13:58:43 -06:00
|
|
|
|
const bmadDir = path.join(projectDir, bmadFolderName);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// Check for legacy _cfg folder and prompt for rename
|
|
|
|
|
|
if (hasLegacyCfg && !config._quickUpdate) {
|
|
|
|
|
|
spinner.stop();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.yellow('\n⚠️ Legacy configuration folder detected'));
|
|
|
|
|
|
console.log(chalk.dim(` Found: ${path.join(bmadDir, '_cfg')}`));
|
|
|
|
|
|
console.log(chalk.dim(' The configuration folder has been renamed from "_cfg" to "_config"'));
|
|
|
|
|
|
|
|
|
|
|
|
const inquirer = require('inquirer');
|
|
|
|
|
|
const { shouldRename } = await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'confirm',
|
|
|
|
|
|
name: 'shouldRename',
|
|
|
|
|
|
message: 'Would you like the installer to rename "_cfg" to "_config" for you?',
|
|
|
|
|
|
default: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
if (!shouldRename) {
|
|
|
|
|
|
console.log(chalk.red('\n❌ Installation cancelled'));
|
|
|
|
|
|
console.log(chalk.dim('You must manually rename the "_cfg" folder to "_config" before proceeding.'));
|
|
|
|
|
|
return { success: false, cancelled: true };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Perform the rename
|
|
|
|
|
|
spinner.start('Renaming configuration folder...');
|
|
|
|
|
|
try {
|
|
|
|
|
|
const oldCfgPath = path.join(bmadDir, '_cfg');
|
|
|
|
|
|
const newCfgPath = path.join(bmadDir, '_config');
|
|
|
|
|
|
await fs.move(oldCfgPath, newCfgPath);
|
|
|
|
|
|
spinner.succeed('Configuration folder renamed successfully');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
spinner.fail('Failed to rename configuration folder');
|
|
|
|
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
|
|
|
|
return { success: false, error: error.message };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Check existing installation
|
|
|
|
|
|
spinner.text = 'Checking for existing installation...';
|
|
|
|
|
|
const existingInstall = await this.detector.detect(bmadDir);
|
|
|
|
|
|
|
2025-10-26 16:17:37 -05:00
|
|
|
|
if (existingInstall.installed && !config.force && !config._quickUpdate) {
|
2025-09-28 23:17:07 -05:00
|
|
|
|
spinner.stop();
|
|
|
|
|
|
|
2025-10-28 12:47:45 -05:00
|
|
|
|
// Check if user already decided what to do (from early menu in ui.js)
|
|
|
|
|
|
let action = null;
|
|
|
|
|
|
if (config._requestedReinstall) {
|
|
|
|
|
|
action = 'reinstall';
|
|
|
|
|
|
} else if (config.actionType === 'update') {
|
|
|
|
|
|
action = 'update';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Fallback: Ask the user (backwards compatibility for other code paths)
|
|
|
|
|
|
console.log(chalk.yellow('\n⚠️ Existing BMAD installation detected'));
|
|
|
|
|
|
console.log(chalk.dim(` Location: ${bmadDir}`));
|
|
|
|
|
|
console.log(chalk.dim(` Version: ${existingInstall.version}`));
|
|
|
|
|
|
|
|
|
|
|
|
const promptResult = await this.promptUpdateAction();
|
|
|
|
|
|
action = promptResult.action;
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
|
|
|
|
|
if (action === 'cancel') {
|
|
|
|
|
|
console.log('Installation cancelled.');
|
2025-10-04 19:46:16 -05:00
|
|
|
|
return { success: false, cancelled: true };
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
2025-09-30 20:06:02 -05:00
|
|
|
|
|
|
|
|
|
|
if (action === 'reinstall') {
|
|
|
|
|
|
// Warn about destructive operation
|
|
|
|
|
|
console.log(chalk.red.bold('\n⚠️ WARNING: This is a destructive operation!'));
|
|
|
|
|
|
console.log(chalk.red('All custom files and modifications in the bmad directory will be lost.'));
|
|
|
|
|
|
|
|
|
|
|
|
const inquirer = require('inquirer');
|
|
|
|
|
|
const { confirmReinstall } = await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'confirm',
|
|
|
|
|
|
name: 'confirmReinstall',
|
|
|
|
|
|
message: chalk.yellow('Are you sure you want to delete and reinstall?'),
|
|
|
|
|
|
default: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
if (!confirmReinstall) {
|
|
|
|
|
|
console.log('Installation cancelled.');
|
2025-10-04 19:46:16 -05:00
|
|
|
|
return { success: false, cancelled: true };
|
2025-09-30 20:06:02 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-04 19:54:47 -05:00
|
|
|
|
// Remember previously configured IDEs before deleting
|
|
|
|
|
|
config._previouslyConfiguredIdes = existingInstall.ides || [];
|
|
|
|
|
|
|
2025-09-30 20:06:02 -05:00
|
|
|
|
// Remove existing installation
|
|
|
|
|
|
await fs.remove(bmadDir);
|
|
|
|
|
|
console.log(chalk.green('✓ Removed existing installation\n'));
|
2025-10-04 19:46:16 -05:00
|
|
|
|
|
|
|
|
|
|
// Mark this as a full reinstall so we re-collect IDE configurations
|
|
|
|
|
|
config._isFullReinstall = true;
|
2025-09-30 20:06:02 -05:00
|
|
|
|
} else if (action === 'update') {
|
|
|
|
|
|
// Store that we're updating for later processing
|
|
|
|
|
|
config._isUpdate = true;
|
|
|
|
|
|
config._existingInstall = existingInstall;
|
|
|
|
|
|
|
2025-09-30 21:20:13 -05:00
|
|
|
|
// Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
|
|
|
|
|
|
const existingFilesManifest = await this.readFilesManifest(bmadDir);
|
|
|
|
|
|
console.log(chalk.dim(`DEBUG: Read ${existingFilesManifest.length} files from manifest`));
|
|
|
|
|
|
console.log(chalk.dim(`DEBUG: Manifest has hashes: ${existingFilesManifest.some((f) => f.hash)}`));
|
|
|
|
|
|
|
|
|
|
|
|
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.dim(`DEBUG: Found ${customFiles.length} custom files, ${modifiedFiles.length} modified files`));
|
|
|
|
|
|
if (modifiedFiles.length > 0) {
|
|
|
|
|
|
console.log(chalk.yellow('DEBUG: Modified files:'));
|
|
|
|
|
|
for (const f of modifiedFiles) console.log(chalk.dim(` - ${f.path}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
config._customFiles = customFiles;
|
|
|
|
|
|
config._modifiedFiles = modifiedFiles;
|
2025-09-30 20:06:02 -05:00
|
|
|
|
|
|
|
|
|
|
// If there are custom files, back them up temporarily
|
2025-09-30 21:20:13 -05:00
|
|
|
|
if (customFiles.length > 0) {
|
2025-12-13 16:22:34 +08:00
|
|
|
|
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
|
2025-09-30 20:06:02 -05:00
|
|
|
|
await fs.ensureDir(tempBackupDir);
|
|
|
|
|
|
|
2025-09-30 21:20:13 -05:00
|
|
|
|
spinner.start(`Backing up ${customFiles.length} custom files...`);
|
|
|
|
|
|
for (const customFile of customFiles) {
|
2025-09-30 20:06:02 -05:00
|
|
|
|
const relativePath = path.relative(bmadDir, customFile);
|
|
|
|
|
|
const backupPath = path.join(tempBackupDir, relativePath);
|
|
|
|
|
|
await fs.ensureDir(path.dirname(backupPath));
|
|
|
|
|
|
await fs.copy(customFile, backupPath);
|
|
|
|
|
|
}
|
2025-09-30 21:20:13 -05:00
|
|
|
|
spinner.succeed(`Backed up ${customFiles.length} custom files`);
|
2025-09-30 20:06:02 -05:00
|
|
|
|
|
|
|
|
|
|
config._tempBackupDir = tempBackupDir;
|
|
|
|
|
|
}
|
2025-09-30 21:20:13 -05:00
|
|
|
|
|
|
|
|
|
|
// For modified files, back them up to temp directory (will be restored as .bak files after install)
|
|
|
|
|
|
if (modifiedFiles.length > 0) {
|
2025-12-13 16:22:34 +08:00
|
|
|
|
const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
|
2025-09-30 21:20:13 -05:00
|
|
|
|
await fs.ensureDir(tempModifiedBackupDir);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.yellow(`\nDEBUG: Backing up ${modifiedFiles.length} modified files to temp location`));
|
|
|
|
|
|
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
|
|
|
|
|
|
for (const modifiedFile of modifiedFiles) {
|
|
|
|
|
|
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
|
|
|
|
|
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
|
|
|
|
|
|
console.log(chalk.dim(`DEBUG: Backing up ${relativePath} to temp`));
|
|
|
|
|
|
await fs.ensureDir(path.dirname(tempBackupPath));
|
|
|
|
|
|
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
|
|
|
|
|
|
|
|
|
|
|
|
config._tempModifiedBackupDir = tempModifiedBackupDir;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.dim('DEBUG: No modified files detected'));
|
|
|
|
|
|
}
|
2025-09-30 20:06:02 -05:00
|
|
|
|
}
|
2025-10-26 16:17:37 -05:00
|
|
|
|
} else if (existingInstall.installed && config._quickUpdate) {
|
|
|
|
|
|
// Quick update mode - automatically treat as update without prompting
|
|
|
|
|
|
spinner.text = 'Preparing quick update...';
|
|
|
|
|
|
config._isUpdate = true;
|
|
|
|
|
|
config._existingInstall = existingInstall;
|
|
|
|
|
|
|
|
|
|
|
|
// Detect custom and modified files BEFORE updating
|
|
|
|
|
|
const existingFilesManifest = await this.readFilesManifest(bmadDir);
|
|
|
|
|
|
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
|
|
|
|
|
|
|
|
|
|
|
|
config._customFiles = customFiles;
|
|
|
|
|
|
config._modifiedFiles = modifiedFiles;
|
|
|
|
|
|
|
|
|
|
|
|
// Back up custom files
|
|
|
|
|
|
if (customFiles.length > 0) {
|
2025-12-13 16:22:34 +08:00
|
|
|
|
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
|
2025-10-26 16:17:37 -05:00
|
|
|
|
await fs.ensureDir(tempBackupDir);
|
|
|
|
|
|
|
|
|
|
|
|
spinner.start(`Backing up ${customFiles.length} custom files...`);
|
|
|
|
|
|
for (const customFile of customFiles) {
|
|
|
|
|
|
const relativePath = path.relative(bmadDir, customFile);
|
|
|
|
|
|
const backupPath = path.join(tempBackupDir, relativePath);
|
|
|
|
|
|
await fs.ensureDir(path.dirname(backupPath));
|
|
|
|
|
|
await fs.copy(customFile, backupPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
spinner.succeed(`Backed up ${customFiles.length} custom files`);
|
|
|
|
|
|
config._tempBackupDir = tempBackupDir;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Back up modified files
|
|
|
|
|
|
if (modifiedFiles.length > 0) {
|
2025-12-13 16:22:34 +08:00
|
|
|
|
const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
|
2025-10-26 16:17:37 -05:00
|
|
|
|
await fs.ensureDir(tempModifiedBackupDir);
|
|
|
|
|
|
|
|
|
|
|
|
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
|
|
|
|
|
|
for (const modifiedFile of modifiedFiles) {
|
|
|
|
|
|
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
|
|
|
|
|
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
|
|
|
|
|
|
await fs.ensureDir(path.dirname(tempBackupPath));
|
|
|
|
|
|
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
|
|
|
|
|
|
config._tempModifiedBackupDir = tempModifiedBackupDir;
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-04 19:46:16 -05:00
|
|
|
|
// Now collect tool configurations after we know if it's a reinstall
|
2025-10-26 16:17:37 -05:00
|
|
|
|
// Skip for quick update since we already have the IDE list
|
2025-10-04 19:46:16 -05:00
|
|
|
|
spinner.stop();
|
2025-10-26 16:17:37 -05:00
|
|
|
|
let toolSelection;
|
|
|
|
|
|
if (config._quickUpdate) {
|
2025-10-26 17:04:27 -05:00
|
|
|
|
// Quick update already has IDEs configured, use saved configurations
|
2025-10-26 16:17:37 -05:00
|
|
|
|
const preConfiguredIdes = {};
|
2025-10-26 17:04:27 -05:00
|
|
|
|
const savedIdeConfigs = config._savedIdeConfigs || {};
|
|
|
|
|
|
|
2025-10-26 16:17:37 -05:00
|
|
|
|
for (const ide of config.ides || []) {
|
2025-10-26 17:04:27 -05:00
|
|
|
|
// Use saved config if available, otherwise mark as already configured (legacy)
|
|
|
|
|
|
if (savedIdeConfigs[ide]) {
|
|
|
|
|
|
preConfiguredIdes[ide] = savedIdeConfigs[ide];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
preConfiguredIdes[ide] = { _alreadyConfigured: true };
|
|
|
|
|
|
}
|
2025-10-26 16:17:37 -05:00
|
|
|
|
}
|
|
|
|
|
|
toolSelection = {
|
|
|
|
|
|
ides: config.ides || [],
|
|
|
|
|
|
skipIde: !config.ides || config.ides.length === 0,
|
|
|
|
|
|
configurations: preConfiguredIdes,
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
2025-10-28 12:47:45 -05:00
|
|
|
|
// Pass pre-selected IDEs from early prompt (if available)
|
|
|
|
|
|
// This allows IDE selection to happen before file copying, improving UX
|
|
|
|
|
|
const preSelectedIdes = config.ides && config.ides.length > 0 ? config.ides : null;
|
2025-10-26 16:17:37 -05:00
|
|
|
|
toolSelection = await this.collectToolConfigurations(
|
|
|
|
|
|
path.resolve(config.directory),
|
|
|
|
|
|
config.modules,
|
|
|
|
|
|
config._isFullReinstall || false,
|
|
|
|
|
|
config._previouslyConfiguredIdes || [],
|
2025-10-28 12:47:45 -05:00
|
|
|
|
preSelectedIdes,
|
2025-10-26 16:17:37 -05:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-10-04 19:46:16 -05:00
|
|
|
|
|
2025-10-26 16:17:37 -05:00
|
|
|
|
// Merge tool selection into config (for both quick update and regular flow)
|
2025-10-04 19:46:16 -05:00
|
|
|
|
config.ides = toolSelection.ides;
|
|
|
|
|
|
config.skipIde = toolSelection.skipIde;
|
|
|
|
|
|
const ideConfigurations = toolSelection.configurations;
|
|
|
|
|
|
|
2025-11-13 18:58:50 -06:00
|
|
|
|
// Check if spinner is already running (e.g., from folder name change scenario)
|
|
|
|
|
|
if (spinner.isSpinning) {
|
|
|
|
|
|
spinner.text = 'Continuing installation...';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
spinner.start('Continuing installation...');
|
|
|
|
|
|
}
|
2025-10-04 19:46:16 -05:00
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Create bmad directory structure
|
|
|
|
|
|
spinner.text = 'Creating directory structure...';
|
|
|
|
|
|
await this.createDirectoryStructure(bmadDir);
|
|
|
|
|
|
|
2025-12-07 20:46:09 -06:00
|
|
|
|
// Get project root
|
2025-09-28 23:17:07 -05:00
|
|
|
|
const projectRoot = getProjectRoot();
|
2025-12-07 17:17:50 -06:00
|
|
|
|
|
2025-12-07 20:46:09 -06:00
|
|
|
|
// Step 1: Install core module first (if requested)
|
|
|
|
|
|
if (config.installCore) {
|
|
|
|
|
|
spinner.start('Installing BMAD core...');
|
|
|
|
|
|
await this.installCoreWithDependencies(bmadDir, { core: {} });
|
|
|
|
|
|
spinner.succeed('Core installed');
|
|
|
|
|
|
|
|
|
|
|
|
// Generate core config file
|
|
|
|
|
|
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Custom content is already handled in UI before module selection
|
|
|
|
|
|
let finalCustomContent = config.customContent;
|
|
|
|
|
|
|
|
|
|
|
|
// Step 3: Prepare modules list including cached custom modules
|
2025-12-07 17:17:50 -06:00
|
|
|
|
let allModules = [...(config.modules || [])];
|
2025-12-07 20:46:09 -06:00
|
|
|
|
|
|
|
|
|
|
// During quick update, we might have custom module sources from the manifest
|
|
|
|
|
|
if (config._customModuleSources) {
|
|
|
|
|
|
// Add custom modules from stored sources
|
|
|
|
|
|
for (const [moduleId, customInfo] of config._customModuleSources) {
|
|
|
|
|
|
if (!allModules.includes(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
|
|
|
|
|
|
allModules.push(moduleId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add cached custom modules
|
|
|
|
|
|
if (finalCustomContent && finalCustomContent.cachedModules) {
|
|
|
|
|
|
for (const cachedModule of finalCustomContent.cachedModules) {
|
|
|
|
|
|
if (!allModules.includes(cachedModule.id)) {
|
|
|
|
|
|
allModules.push(cachedModule.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Regular custom content from user input (non-cached)
|
|
|
|
|
|
if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
|
2025-12-07 17:17:50 -06:00
|
|
|
|
// Add custom modules to the installation list
|
2025-12-08 12:24:30 -07:00
|
|
|
|
const customHandler = new CustomHandler();
|
2025-12-07 20:46:09 -06:00
|
|
|
|
for (const customFile of finalCustomContent.selectedFiles) {
|
2025-12-07 17:17:50 -06:00
|
|
|
|
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
|
|
|
|
|
|
if (customInfo && customInfo.id) {
|
|
|
|
|
|
allModules.push(customInfo.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 20:46:09 -06:00
|
|
|
|
// Don't include core again if already installed
|
|
|
|
|
|
if (config.installCore) {
|
|
|
|
|
|
allModules = allModules.filter((m) => m !== 'core');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const modulesToInstall = allModules;
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
|
|
|
|
|
// For dependency resolution, we need to pass the project root
|
2025-12-07 17:17:50 -06:00
|
|
|
|
// Create a temporary module manager that knows about custom content locations
|
|
|
|
|
|
const tempModuleManager = new ModuleManager({
|
|
|
|
|
|
scanProjectForModules: true,
|
2025-12-07 20:46:09 -06:00
|
|
|
|
bmadDir: bmadDir, // Pass bmadDir so we can check cache
|
2025-12-07 17:17:50 -06:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Make sure custom modules are discoverable
|
|
|
|
|
|
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
|
|
|
|
|
|
// The dependency resolver needs to know about these modules
|
|
|
|
|
|
// We'll handle custom modules separately in the installation loop
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resolution = await this.dependencyResolver.resolve(projectRoot, allModules, {
|
|
|
|
|
|
verbose: config.verbose,
|
|
|
|
|
|
moduleManager: tempModuleManager,
|
|
|
|
|
|
});
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
|
|
|
|
|
if (config.verbose) {
|
|
|
|
|
|
spinner.succeed('Dependencies resolved');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
spinner.succeed('Dependencies resolved');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 20:46:09 -06:00
|
|
|
|
// Core is already installed above, skip if included in resolution
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-12-06 15:28:37 -06:00
|
|
|
|
// Install modules with their dependencies
|
2025-12-07 17:17:50 -06:00
|
|
|
|
if (allModules && allModules.length > 0) {
|
|
|
|
|
|
const installedModuleNames = new Set();
|
|
|
|
|
|
|
|
|
|
|
|
for (const moduleName of allModules) {
|
|
|
|
|
|
// Skip if already installed
|
|
|
|
|
|
if (installedModuleNames.has(moduleName)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
installedModuleNames.add(moduleName);
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
spinner.start(`Installing module: ${moduleName}...`);
|
2025-12-07 17:17:50 -06:00
|
|
|
|
|
|
|
|
|
|
// Check if this is a custom module
|
|
|
|
|
|
let isCustomModule = false;
|
|
|
|
|
|
let customInfo = null;
|
2025-12-07 20:46:09 -06:00
|
|
|
|
let useCache = false;
|
|
|
|
|
|
|
|
|
|
|
|
// First check if we have a cached version
|
|
|
|
|
|
if (finalCustomContent && finalCustomContent.cachedModules) {
|
|
|
|
|
|
const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
|
|
|
|
|
|
if (cachedModule) {
|
|
|
|
|
|
isCustomModule = true;
|
|
|
|
|
|
customInfo = {
|
|
|
|
|
|
id: moduleName,
|
|
|
|
|
|
path: cachedModule.cachePath,
|
|
|
|
|
|
config: {},
|
|
|
|
|
|
};
|
|
|
|
|
|
useCache = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Then check if we have custom module sources from the manifest (for quick update)
|
|
|
|
|
|
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
|
|
|
|
|
|
customInfo = config._customModuleSources.get(moduleName);
|
|
|
|
|
|
isCustomModule = true;
|
|
|
|
|
|
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// Check if this is a cached module (source path starts with _config)
|
|
|
|
|
|
if (
|
|
|
|
|
|
customInfo.sourcePath &&
|
|
|
|
|
|
(customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom'))
|
|
|
|
|
|
) {
|
2025-12-07 20:46:09 -06:00
|
|
|
|
useCache = true;
|
|
|
|
|
|
// Make sure we have the right path structure
|
|
|
|
|
|
if (!customInfo.path) {
|
|
|
|
|
|
customInfo.path = customInfo.sourcePath;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Finally check regular custom content
|
|
|
|
|
|
if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
|
2025-12-07 17:17:50 -06:00
|
|
|
|
const customHandler = new CustomHandler();
|
2025-12-07 20:46:09 -06:00
|
|
|
|
for (const customFile of finalCustomContent.selectedFiles) {
|
2025-12-07 17:17:50 -06:00
|
|
|
|
const info = await customHandler.getCustomInfo(customFile, projectDir);
|
|
|
|
|
|
if (info && info.id === moduleName) {
|
|
|
|
|
|
isCustomModule = true;
|
|
|
|
|
|
customInfo = info;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isCustomModule && customInfo) {
|
|
|
|
|
|
// Install custom module using CustomHandler but as a proper module
|
|
|
|
|
|
const customHandler = new CustomHandler();
|
|
|
|
|
|
|
|
|
|
|
|
// Install to module directory instead of custom directory
|
|
|
|
|
|
const moduleTargetPath = path.join(bmadDir, moduleName);
|
|
|
|
|
|
await fs.ensureDir(moduleTargetPath);
|
|
|
|
|
|
|
2025-12-08 22:33:53 +09:00
|
|
|
|
// Get collected config for this custom module (from module.yaml prompts)
|
|
|
|
|
|
const collectedModuleConfig = moduleConfigs[moduleName] || {};
|
|
|
|
|
|
|
2025-12-07 17:17:50 -06:00
|
|
|
|
const result = await customHandler.install(
|
|
|
|
|
|
customInfo.path,
|
|
|
|
|
|
path.join(bmadDir, 'temp-custom'),
|
2025-12-08 22:33:53 +09:00
|
|
|
|
{ ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig, _bmadDir: bmadDir },
|
2025-12-07 17:17:50 -06:00
|
|
|
|
(filePath) => {
|
|
|
|
|
|
// Track installed files with correct path
|
|
|
|
|
|
const relativePath = path.relative(path.join(bmadDir, 'temp-custom'), filePath);
|
|
|
|
|
|
const finalPath = path.join(moduleTargetPath, relativePath);
|
|
|
|
|
|
this.installedFiles.push(finalPath);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Move from temp-custom to actual module directory
|
|
|
|
|
|
const tempCustomPath = path.join(bmadDir, 'temp-custom');
|
|
|
|
|
|
if (await fs.pathExists(tempCustomPath)) {
|
|
|
|
|
|
const customDir = path.join(tempCustomPath, 'custom');
|
|
|
|
|
|
if (await fs.pathExists(customDir)) {
|
|
|
|
|
|
// Move contents to module directory
|
|
|
|
|
|
const items = await fs.readdir(customDir);
|
2025-12-08 12:24:30 -07:00
|
|
|
|
const movedItems = [];
|
|
|
|
|
|
try {
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
|
const srcPath = path.join(customDir, item);
|
|
|
|
|
|
const destPath = path.join(moduleTargetPath, item);
|
2025-12-07 17:17:50 -06:00
|
|
|
|
|
2025-12-08 12:24:30 -07:00
|
|
|
|
// If destination exists, remove it first (or we could merge)
|
|
|
|
|
|
if (await fs.pathExists(destPath)) {
|
|
|
|
|
|
await fs.remove(destPath);
|
|
|
|
|
|
}
|
2025-12-07 17:17:50 -06:00
|
|
|
|
|
2025-12-08 12:24:30 -07:00
|
|
|
|
await fs.move(srcPath, destPath);
|
|
|
|
|
|
movedItems.push({ src: srcPath, dest: destPath });
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (moveError) {
|
|
|
|
|
|
// Rollback: restore any successfully moved items
|
|
|
|
|
|
for (const moved of movedItems) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await fs.move(moved.dest, moved.src);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Best-effort rollback - log if it fails
|
|
|
|
|
|
console.error(`Failed to rollback ${moved.dest} during cleanup`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new Error(`Failed to move custom module files: ${moveError.message}`);
|
2025-12-07 17:17:50 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-08 12:24:30 -07:00
|
|
|
|
try {
|
|
|
|
|
|
await fs.remove(tempCustomPath);
|
|
|
|
|
|
} catch (cleanupError) {
|
|
|
|
|
|
// Non-fatal: temp directory cleanup failed but files were moved successfully
|
|
|
|
|
|
console.warn(`Warning: Could not clean up temp directory: ${cleanupError.message}`);
|
|
|
|
|
|
}
|
2025-12-07 17:17:50 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-08 22:33:53 +09:00
|
|
|
|
// Create module config (include collected config from module.yaml prompts)
|
|
|
|
|
|
await this.generateModuleConfigs(bmadDir, {
|
|
|
|
|
|
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
|
|
|
|
|
});
|
2025-12-07 20:46:09 -06:00
|
|
|
|
|
|
|
|
|
|
// Store custom module info for later manifest update
|
|
|
|
|
|
if (!config._customModulesToTrack) {
|
|
|
|
|
|
config._customModulesToTrack = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// For cached modules, use appropriate path handling
|
|
|
|
|
|
let sourcePath;
|
|
|
|
|
|
if (useCache) {
|
|
|
|
|
|
// Check if we have cached modules info (from initial install)
|
|
|
|
|
|
if (finalCustomContent && finalCustomContent.cachedModules) {
|
|
|
|
|
|
sourcePath = finalCustomContent.cachedModules.find((m) => m.id === moduleName)?.relativePath;
|
|
|
|
|
|
} else {
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// During update, the sourcePath is already cache-relative if it starts with _config
|
2025-12-07 20:46:09 -06:00
|
|
|
|
sourcePath =
|
2025-12-13 19:41:09 +08:00
|
|
|
|
customInfo.sourcePath && customInfo.sourcePath.startsWith('_config')
|
2025-12-07 20:46:09 -06:00
|
|
|
|
? customInfo.sourcePath
|
|
|
|
|
|
: path.relative(bmadDir, customInfo.path || customInfo.sourcePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sourcePath = path.resolve(customInfo.path || customInfo.sourcePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
config._customModulesToTrack.push({
|
|
|
|
|
|
id: customInfo.id,
|
|
|
|
|
|
name: customInfo.name,
|
|
|
|
|
|
sourcePath: sourcePath,
|
|
|
|
|
|
installDate: new Date().toISOString(),
|
|
|
|
|
|
});
|
2025-12-07 17:17:50 -06:00
|
|
|
|
} else {
|
|
|
|
|
|
// Regular module installation
|
2025-12-07 20:46:09 -06:00
|
|
|
|
// Special case for core module
|
|
|
|
|
|
if (moduleName === 'core') {
|
|
|
|
|
|
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
|
|
|
|
|
|
}
|
2025-12-07 17:17:50 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
spinner.succeed(`Module installed: ${moduleName}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 15:28:37 -06:00
|
|
|
|
// Install partial modules (only dependencies)
|
2025-09-28 23:17:07 -05:00
|
|
|
|
for (const [module, files] of Object.entries(resolution.byModule)) {
|
2025-12-07 17:17:50 -06:00
|
|
|
|
if (!allModules.includes(module) && module !== 'core') {
|
2025-10-26 19:38:38 -05:00
|
|
|
|
const totalFiles =
|
|
|
|
|
|
files.agents.length +
|
|
|
|
|
|
files.tasks.length +
|
|
|
|
|
|
files.tools.length +
|
|
|
|
|
|
files.templates.length +
|
|
|
|
|
|
files.data.length +
|
|
|
|
|
|
files.other.length;
|
2025-09-28 23:17:07 -05:00
|
|
|
|
if (totalFiles > 0) {
|
|
|
|
|
|
spinner.start(`Installing ${module} dependencies...`);
|
|
|
|
|
|
await this.installPartialModule(module, bmadDir, files);
|
|
|
|
|
|
spinner.succeed(`${module} dependencies installed`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 13:39:03 -06:00
|
|
|
|
// Install custom content if provided AND selected
|
2025-12-07 17:17:50 -06:00
|
|
|
|
// Process custom content that wasn't installed as modules
|
|
|
|
|
|
// This is now handled in the module installation loop above
|
|
|
|
|
|
// This section is kept for backward compatibility with any custom content
|
|
|
|
|
|
// that doesn't have a module structure
|
|
|
|
|
|
const remainingCustomContent = [];
|
2025-12-07 13:39:03 -06:00
|
|
|
|
if (
|
|
|
|
|
|
config.customContent &&
|
|
|
|
|
|
config.customContent.hasCustomContent &&
|
|
|
|
|
|
config.customContent.customPath &&
|
|
|
|
|
|
config.customContent.selected &&
|
|
|
|
|
|
config.customContent.selectedFiles
|
|
|
|
|
|
) {
|
2025-12-07 17:17:50 -06:00
|
|
|
|
// Filter out custom modules that were already installed
|
2025-12-08 12:24:30 -07:00
|
|
|
|
const customHandler = new CustomHandler();
|
2025-12-07 17:17:50 -06:00
|
|
|
|
for (const customFile of config.customContent.selectedFiles) {
|
|
|
|
|
|
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Skip if this was installed as a module
|
|
|
|
|
|
if (!customInfo || !customInfo.id || !allModules.includes(customInfo.id)) {
|
|
|
|
|
|
remainingCustomContent.push(customFile);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (remainingCustomContent.length > 0) {
|
|
|
|
|
|
spinner.start('Installing remaining custom content...');
|
2025-12-07 13:39:03 -06:00
|
|
|
|
const customHandler = new CustomHandler();
|
|
|
|
|
|
|
2025-12-07 17:17:50 -06:00
|
|
|
|
// Use the remaining files
|
|
|
|
|
|
const customFiles = remainingCustomContent;
|
2025-12-07 13:39:03 -06:00
|
|
|
|
|
|
|
|
|
|
if (customFiles.length > 0) {
|
|
|
|
|
|
console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`));
|
|
|
|
|
|
for (const customFile of customFiles) {
|
|
|
|
|
|
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
|
|
|
|
|
|
if (customInfo) {
|
|
|
|
|
|
console.log(chalk.dim(` • ${customInfo.name} (${customInfo.relativePath})`));
|
|
|
|
|
|
|
|
|
|
|
|
// Install the custom content
|
|
|
|
|
|
const result = await customHandler.install(
|
|
|
|
|
|
customInfo.path,
|
|
|
|
|
|
bmadDir,
|
|
|
|
|
|
{ ...config.coreConfig, ...customInfo.config },
|
|
|
|
|
|
(filePath) => {
|
|
|
|
|
|
// Track installed files
|
|
|
|
|
|
this.installedFiles.push(filePath);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (result.errors.length > 0) {
|
|
|
|
|
|
console.log(chalk.yellow(` ⚠️ ${result.errors.length} error(s) occurred`));
|
|
|
|
|
|
for (const error of result.errors) {
|
|
|
|
|
|
console.log(chalk.dim(` - ${error}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.green(` ✓ Installed ${result.agentsInstalled} agents, ${result.workflowsInstalled} workflows`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
spinner.succeed('Custom content installed');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Generate clean config.yaml files for each installed module
|
|
|
|
|
|
spinner.start('Generating module configurations...');
|
|
|
|
|
|
await this.generateModuleConfigs(bmadDir, moduleConfigs);
|
|
|
|
|
|
spinner.succeed('Module configurations generated');
|
|
|
|
|
|
|
|
|
|
|
|
// Create agent configuration files
|
2025-10-02 21:45:59 -05:00
|
|
|
|
// Note: Legacy createAgentConfigs removed - using YAML customize system instead
|
|
|
|
|
|
// Customize templates are now created in processAgentFiles when building YAML agents
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-09-30 21:20:13 -05:00
|
|
|
|
// Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion)
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const cfgDir = path.join(bmadDir, '_config');
|
2025-09-30 21:20:13 -05:00
|
|
|
|
this.installedFiles.push(
|
|
|
|
|
|
path.join(cfgDir, 'manifest.yaml'),
|
|
|
|
|
|
path.join(cfgDir, 'workflow-manifest.csv'),
|
|
|
|
|
|
path.join(cfgDir, 'agent-manifest.csv'),
|
|
|
|
|
|
path.join(cfgDir, 'task-manifest.csv'),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup
|
2025-09-28 23:17:07 -05:00
|
|
|
|
spinner.start('Generating workflow and agent manifests...');
|
|
|
|
|
|
const manifestGen = new ManifestGenerator();
|
2025-10-26 16:17:37 -05:00
|
|
|
|
|
2025-12-07 20:46:09 -06:00
|
|
|
|
// For quick update, we need ALL installed modules in the manifest
|
|
|
|
|
|
// Not just the ones being updated
|
|
|
|
|
|
const allModulesForManifest = config._quickUpdate
|
|
|
|
|
|
? config._existingModules || allModules || []
|
|
|
|
|
|
: config._preserveModules
|
|
|
|
|
|
? [...allModules, ...config._preserveModules]
|
|
|
|
|
|
: allModules || [];
|
|
|
|
|
|
|
|
|
|
|
|
// For regular installs (including when called from quick update), use what we have
|
|
|
|
|
|
let modulesForCsvPreserve;
|
|
|
|
|
|
if (config._quickUpdate) {
|
|
|
|
|
|
// Quick update - use existing modules or fall back to modules being updated
|
|
|
|
|
|
modulesForCsvPreserve = config._existingModules || allModules || [];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Regular install - use the modules we're installing plus any preserved ones
|
|
|
|
|
|
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
|
|
|
|
|
|
}
|
2025-10-26 16:17:37 -05:00
|
|
|
|
|
2025-12-07 20:46:09 -06:00
|
|
|
|
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, this.installedFiles, {
|
2025-10-06 12:08:36 -07:00
|
|
|
|
ides: config.ides || [],
|
2025-12-07 20:46:09 -06:00
|
|
|
|
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
|
2025-10-06 12:08:36 -07:00
|
|
|
|
});
|
2025-09-30 21:20:13 -05:00
|
|
|
|
|
2025-12-07 20:46:09 -06:00
|
|
|
|
// Add custom modules to manifest (now that it exists)
|
|
|
|
|
|
if (config._customModulesToTrack && config._customModulesToTrack.length > 0) {
|
|
|
|
|
|
spinner.text = 'Storing custom module sources...';
|
|
|
|
|
|
for (const customModule of config._customModulesToTrack) {
|
|
|
|
|
|
await this.manifest.addCustomModule(bmadDir, customModule);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
spinner.succeed(
|
2025-10-26 19:38:38 -05:00
|
|
|
|
`Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
|
2025-09-28 23:17:07 -05:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Configure IDEs and copy documentation
|
|
|
|
|
|
if (!config.skipIde && config.ides && config.ides.length > 0) {
|
2025-10-26 19:38:38 -05:00
|
|
|
|
// Filter out any undefined/null values from the IDE list
|
|
|
|
|
|
const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
|
2025-10-26 17:04:27 -05:00
|
|
|
|
|
2025-10-26 19:38:38 -05:00
|
|
|
|
if (validIdes.length === 0) {
|
|
|
|
|
|
console.log(chalk.yellow('⚠️ No valid IDEs selected. Skipping IDE configuration.'));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Check if any IDE might need prompting (no pre-collected config)
|
|
|
|
|
|
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-10-26 19:38:38 -05:00
|
|
|
|
if (!needsPrompting) {
|
|
|
|
|
|
spinner.start('Configuring IDEs...');
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-10-26 19:38:38 -05:00
|
|
|
|
// Temporarily suppress console output if not verbose
|
|
|
|
|
|
const originalLog = console.log;
|
|
|
|
|
|
if (!config.verbose) {
|
|
|
|
|
|
console.log = () => {};
|
2025-10-26 17:04:27 -05:00
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-10-26 19:38:38 -05:00
|
|
|
|
for (const ide of validIdes) {
|
|
|
|
|
|
// Only show spinner if we have pre-collected config (no prompts expected)
|
|
|
|
|
|
if (ideConfigurations[ide] && !needsPrompting) {
|
|
|
|
|
|
spinner.text = `Configuring ${ide}...`;
|
|
|
|
|
|
} else if (!ideConfigurations[ide]) {
|
|
|
|
|
|
// Stop spinner before prompting
|
|
|
|
|
|
if (spinner.isSpinning) {
|
|
|
|
|
|
spinner.stop();
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log(chalk.cyan(`\nConfiguring ${ide}...`));
|
|
|
|
|
|
}
|
2025-10-26 17:04:27 -05:00
|
|
|
|
|
2025-10-26 19:38:38 -05:00
|
|
|
|
// Pass pre-collected configuration to avoid re-prompting
|
|
|
|
|
|
await this.ideManager.setup(ide, projectDir, bmadDir, {
|
|
|
|
|
|
selectedModules: config.modules || [],
|
|
|
|
|
|
preCollectedConfig: ideConfigurations[ide] || null,
|
|
|
|
|
|
verbose: config.verbose,
|
|
|
|
|
|
});
|
2025-10-26 17:04:27 -05:00
|
|
|
|
|
2025-10-26 19:38:38 -05:00
|
|
|
|
// Save IDE configuration for future updates
|
|
|
|
|
|
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
|
|
|
|
|
|
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Restart spinner if we stopped it
|
|
|
|
|
|
if (!ideConfigurations[ide] && !spinner.isSpinning) {
|
|
|
|
|
|
spinner.start('Configuring IDEs...');
|
|
|
|
|
|
}
|
2025-10-26 17:04:27 -05:00
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-10-26 19:38:38 -05:00
|
|
|
|
// Restore console.log
|
|
|
|
|
|
console.log = originalLog;
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-10-26 19:38:38 -05:00
|
|
|
|
if (spinner.isSpinning) {
|
|
|
|
|
|
spinner.succeed(`Configured ${validIdes.length} IDE${validIdes.length > 1 ? 's' : ''}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.green(`✓ Configured ${validIdes.length} IDE${validIdes.length > 1 ? 's' : ''}`));
|
|
|
|
|
|
}
|
2025-10-26 17:04:27 -05:00
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-10-26 19:38:38 -05:00
|
|
|
|
// Copy IDE-specific documentation (only for valid IDEs)
|
|
|
|
|
|
const validIdesForDocs = (config.ides || []).filter((ide) => ide && typeof ide === 'string');
|
|
|
|
|
|
if (validIdesForDocs.length > 0) {
|
|
|
|
|
|
spinner.start('Copying IDE documentation...');
|
|
|
|
|
|
await this.copyIdeDocumentation(validIdesForDocs, bmadDir);
|
|
|
|
|
|
spinner.succeed('IDE documentation copied');
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Run module-specific installers after IDE setup
|
|
|
|
|
|
spinner.start('Running module-specific installers...');
|
|
|
|
|
|
|
|
|
|
|
|
// Run core module installer if core was installed
|
|
|
|
|
|
if (config.installCore || resolution.byModule.core) {
|
|
|
|
|
|
spinner.text = 'Running core module installer...';
|
|
|
|
|
|
|
|
|
|
|
|
await this.moduleManager.runModuleInstaller('core', bmadDir, {
|
|
|
|
|
|
installedIDEs: config.ides || [],
|
|
|
|
|
|
moduleConfig: moduleConfigs.core || {},
|
2025-12-07 01:43:44 -06:00
|
|
|
|
coreConfig: moduleConfigs.core || {},
|
2025-09-28 23:17:07 -05:00
|
|
|
|
logger: {
|
|
|
|
|
|
log: (msg) => console.log(msg),
|
|
|
|
|
|
error: (msg) => console.error(msg),
|
|
|
|
|
|
warn: (msg) => console.warn(msg),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Run installers for user-selected modules
|
|
|
|
|
|
if (config.modules && config.modules.length > 0) {
|
|
|
|
|
|
for (const moduleName of config.modules) {
|
|
|
|
|
|
spinner.text = `Running ${moduleName} module installer...`;
|
|
|
|
|
|
|
|
|
|
|
|
// Pass installed IDEs and module config to module installer
|
|
|
|
|
|
await this.moduleManager.runModuleInstaller(moduleName, bmadDir, {
|
|
|
|
|
|
installedIDEs: config.ides || [],
|
|
|
|
|
|
moduleConfig: moduleConfigs[moduleName] || {},
|
2025-12-07 01:43:44 -06:00
|
|
|
|
coreConfig: moduleConfigs.core || {},
|
2025-09-28 23:17:07 -05:00
|
|
|
|
logger: {
|
|
|
|
|
|
log: (msg) => console.log(msg),
|
|
|
|
|
|
error: (msg) => console.error(msg),
|
|
|
|
|
|
warn: (msg) => console.warn(msg),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
spinner.succeed('Module-specific installers completed');
|
|
|
|
|
|
|
2025-10-03 11:54:32 -05:00
|
|
|
|
// Note: Manifest files are already created by ManifestGenerator above
|
|
|
|
|
|
// No need to create legacy manifest.csv anymore
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-09-30 20:06:02 -05:00
|
|
|
|
// If this was an update, restore custom files
|
|
|
|
|
|
let customFiles = [];
|
2025-09-30 21:20:13 -05:00
|
|
|
|
let modifiedFiles = [];
|
|
|
|
|
|
if (config._isUpdate) {
|
|
|
|
|
|
if (config._customFiles && config._customFiles.length > 0) {
|
|
|
|
|
|
spinner.start(`Restoring ${config._customFiles.length} custom files...`);
|
|
|
|
|
|
|
|
|
|
|
|
for (const originalPath of config._customFiles) {
|
|
|
|
|
|
const relativePath = path.relative(bmadDir, originalPath);
|
|
|
|
|
|
const backupPath = path.join(config._tempBackupDir, relativePath);
|
|
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(backupPath)) {
|
|
|
|
|
|
await fs.ensureDir(path.dirname(originalPath));
|
|
|
|
|
|
await fs.copy(backupPath, originalPath, { overwrite: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-30 20:06:02 -05:00
|
|
|
|
|
2025-09-30 21:20:13 -05:00
|
|
|
|
// Clean up temp backup
|
|
|
|
|
|
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
|
|
|
|
|
|
await fs.remove(config._tempBackupDir);
|
2025-09-30 20:06:02 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 21:20:13 -05:00
|
|
|
|
spinner.succeed(`Restored ${config._customFiles.length} custom files`);
|
|
|
|
|
|
customFiles = config._customFiles;
|
2025-09-30 20:06:02 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 21:20:13 -05:00
|
|
|
|
if (config._modifiedFiles && config._modifiedFiles.length > 0) {
|
|
|
|
|
|
modifiedFiles = config._modifiedFiles;
|
|
|
|
|
|
|
|
|
|
|
|
// Restore modified files as .bak files
|
|
|
|
|
|
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
|
|
|
|
|
|
spinner.start(`Restoring ${modifiedFiles.length} modified files as .bak...`);
|
|
|
|
|
|
|
|
|
|
|
|
for (const modifiedFile of modifiedFiles) {
|
|
|
|
|
|
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
|
|
|
|
|
const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath);
|
|
|
|
|
|
const bakPath = modifiedFile.path + '.bak';
|
|
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(tempBackupPath)) {
|
|
|
|
|
|
await fs.ensureDir(path.dirname(bakPath));
|
|
|
|
|
|
await fs.copy(tempBackupPath, bakPath, { overwrite: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clean up temp backup
|
|
|
|
|
|
await fs.remove(config._tempModifiedBackupDir);
|
|
|
|
|
|
|
|
|
|
|
|
spinner.succeed(`Restored ${modifiedFiles.length} modified files as .bak`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-30 20:06:02 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
spinner.stop();
|
|
|
|
|
|
|
2025-09-30 21:20:13 -05:00
|
|
|
|
// Report custom and modified files if any were found
|
2025-09-30 20:06:02 -05:00
|
|
|
|
if (customFiles.length > 0) {
|
|
|
|
|
|
console.log(chalk.cyan(`\n📁 Custom files preserved: ${customFiles.length}`));
|
|
|
|
|
|
console.log(chalk.dim('The following custom files were found and restored:\n'));
|
|
|
|
|
|
for (const file of customFiles) {
|
|
|
|
|
|
console.log(chalk.dim(` - ${path.relative(bmadDir, file)}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 21:20:13 -05:00
|
|
|
|
if (modifiedFiles.length > 0) {
|
|
|
|
|
|
console.log(chalk.yellow(`\n⚠️ Modified files detected: ${modifiedFiles.length}`));
|
|
|
|
|
|
console.log(chalk.dim('The following files were modified and backed up with .bak extension:\n'));
|
|
|
|
|
|
for (const file of modifiedFiles) {
|
|
|
|
|
|
console.log(chalk.dim(` - ${file.relativePath} → ${file.relativePath}.bak`));
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log(chalk.dim('\nThese files have been updated with the new version.'));
|
|
|
|
|
|
console.log(chalk.dim('Review the .bak files to see your changes and merge if needed.\n'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Display completion message
|
|
|
|
|
|
const { UI } = require('../../../lib/ui');
|
|
|
|
|
|
const ui = new UI();
|
|
|
|
|
|
ui.showInstallSummary({
|
|
|
|
|
|
path: bmadDir,
|
|
|
|
|
|
modules: config.modules,
|
|
|
|
|
|
ides: config.ides,
|
2025-09-30 20:06:02 -05:00
|
|
|
|
customFiles: customFiles.length > 0 ? customFiles : undefined,
|
2025-12-05 17:54:03 -07:00
|
|
|
|
ttsInjectedFiles: this.enableAgentVibes && this.ttsInjectedFiles.length > 0 ? this.ttsInjectedFiles : undefined,
|
|
|
|
|
|
agentVibesEnabled: this.enableAgentVibes || false,
|
2025-09-28 23:17:07 -05: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
|
|
|
|
return {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
path: bmadDir,
|
|
|
|
|
|
modules: config.modules,
|
|
|
|
|
|
ides: config.ides,
|
|
|
|
|
|
needsAgentVibes: this.enableAgentVibes && !config.agentVibesInstalled,
|
|
|
|
|
|
projectDir: projectDir,
|
|
|
|
|
|
};
|
2025-09-28 23:17:07 -05:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
spinner.fail('Installation failed');
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Update existing installation
|
|
|
|
|
|
*/
|
|
|
|
|
|
async update(config) {
|
|
|
|
|
|
const spinner = ora('Checking installation...').start();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-08 13:58:43 -06:00
|
|
|
|
const projectDir = path.resolve(config.directory);
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const { bmadDir } = await this.findBmadDir(projectDir);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
const existingInstall = await this.detector.detect(bmadDir);
|
|
|
|
|
|
|
|
|
|
|
|
if (!existingInstall.installed) {
|
|
|
|
|
|
spinner.fail('No BMAD installation found');
|
|
|
|
|
|
throw new Error(`No BMAD installation found at ${bmadDir}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
spinner.text = 'Analyzing update requirements...';
|
|
|
|
|
|
|
|
|
|
|
|
// Compare versions and determine what needs updating
|
|
|
|
|
|
const currentVersion = existingInstall.version;
|
|
|
|
|
|
const newVersion = require(path.join(getProjectRoot(), 'package.json')).version;
|
|
|
|
|
|
|
2025-12-07 20:46:09 -06:00
|
|
|
|
// Check for custom modules with missing sources before update
|
|
|
|
|
|
const customModuleSources = new Map();
|
|
|
|
|
|
if (existingInstall.customModules) {
|
|
|
|
|
|
for (const customModule of existingInstall.customModules) {
|
|
|
|
|
|
customModuleSources.set(customModule.id, customModule);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (customModuleSources.size > 0) {
|
|
|
|
|
|
spinner.stop();
|
|
|
|
|
|
console.log(chalk.yellow('\nChecking custom module sources before update...'));
|
|
|
|
|
|
|
|
|
|
|
|
const projectRoot = getProjectRoot();
|
|
|
|
|
|
await this.handleMissingCustomSources(
|
|
|
|
|
|
customModuleSources,
|
|
|
|
|
|
bmadDir,
|
|
|
|
|
|
projectRoot,
|
|
|
|
|
|
'update',
|
|
|
|
|
|
existingInstall.modules.map((m) => m.id),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
spinner.start('Preparing update...');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
if (config.dryRun) {
|
|
|
|
|
|
spinner.stop();
|
|
|
|
|
|
console.log(chalk.cyan('\n🔍 Update Preview (Dry Run)\n'));
|
|
|
|
|
|
console.log(chalk.bold('Current version:'), currentVersion);
|
|
|
|
|
|
console.log(chalk.bold('New version:'), newVersion);
|
|
|
|
|
|
console.log(chalk.bold('Core:'), existingInstall.hasCore ? 'Will be updated' : 'Not installed');
|
|
|
|
|
|
|
|
|
|
|
|
if (existingInstall.modules.length > 0) {
|
|
|
|
|
|
console.log(chalk.bold('\nModules to update:'));
|
|
|
|
|
|
for (const mod of existingInstall.modules) {
|
|
|
|
|
|
console.log(` - ${mod.id}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Perform actual update
|
|
|
|
|
|
if (existingInstall.hasCore) {
|
|
|
|
|
|
spinner.text = 'Updating core...';
|
|
|
|
|
|
await this.updateCore(bmadDir, config.force);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const module of existingInstall.modules) {
|
|
|
|
|
|
spinner.text = `Updating module: ${module.id}...`;
|
|
|
|
|
|
await this.moduleManager.update(module.id, bmadDir, config.force);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update manifest
|
|
|
|
|
|
spinner.text = 'Updating manifest...';
|
|
|
|
|
|
await this.manifest.update(bmadDir, {
|
|
|
|
|
|
version: newVersion,
|
|
|
|
|
|
updateDate: new Date().toISOString(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
spinner.succeed('Update complete');
|
|
|
|
|
|
return { success: true };
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
spinner.fail('Update failed');
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get installation status
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getStatus(directory) {
|
2025-11-08 13:58:43 -06:00
|
|
|
|
const projectDir = path.resolve(directory);
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const { bmadDir } = await this.findBmadDir(projectDir);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
return await this.detector.detect(bmadDir);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get available modules
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getAvailableModules() {
|
|
|
|
|
|
return await this.moduleManager.listAvailable();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Uninstall BMAD
|
|
|
|
|
|
*/
|
|
|
|
|
|
async uninstall(directory) {
|
2025-11-08 13:58:43 -06:00
|
|
|
|
const projectDir = path.resolve(directory);
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const { bmadDir } = await this.findBmadDir(projectDir);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(bmadDir)) {
|
|
|
|
|
|
await fs.remove(bmadDir);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clean up IDE configurations
|
2025-11-08 13:58:43 -06:00
|
|
|
|
await this.ideManager.cleanup(projectDir);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
|
|
|
|
|
return { success: true };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Private: Create directory structure
|
|
|
|
|
|
*/
|
|
|
|
|
|
async createDirectoryStructure(bmadDir) {
|
|
|
|
|
|
await fs.ensureDir(bmadDir);
|
2025-12-13 19:41:09 +08:00
|
|
|
|
await fs.ensureDir(path.join(bmadDir, '_config'));
|
|
|
|
|
|
await fs.ensureDir(path.join(bmadDir, '_config', 'agents'));
|
|
|
|
|
|
await fs.ensureDir(path.join(bmadDir, '_config', 'custom'));
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Generate clean config.yaml files for each installed module
|
|
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
|
|
|
|
|
* @param {Object} moduleConfigs - Collected configuration values
|
|
|
|
|
|
*/
|
|
|
|
|
|
async generateModuleConfigs(bmadDir, moduleConfigs) {
|
2025-12-13 18:35:07 +08:00
|
|
|
|
const yaml = require('yaml');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
|
|
|
|
|
// Extract core config values to share with other modules
|
|
|
|
|
|
const coreConfig = moduleConfigs.core || {};
|
|
|
|
|
|
|
|
|
|
|
|
// Get all installed module directories
|
|
|
|
|
|
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
|
|
|
|
const installedModules = entries
|
2025-12-13 19:41:09 +08:00
|
|
|
|
.filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs')
|
2025-09-28 23:17:07 -05:00
|
|
|
|
.map((entry) => entry.name);
|
|
|
|
|
|
|
|
|
|
|
|
// Generate config.yaml for each installed module
|
|
|
|
|
|
for (const moduleName of installedModules) {
|
|
|
|
|
|
const modulePath = path.join(bmadDir, moduleName);
|
|
|
|
|
|
|
|
|
|
|
|
// Get module-specific config or use empty object if none
|
|
|
|
|
|
const config = moduleConfigs[moduleName] || {};
|
|
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(modulePath)) {
|
|
|
|
|
|
const configPath = path.join(modulePath, 'config.yaml');
|
|
|
|
|
|
|
|
|
|
|
|
// Create header
|
|
|
|
|
|
const packageJson = require(path.join(getProjectRoot(), 'package.json'));
|
|
|
|
|
|
const header = `# ${moduleName.toUpperCase()} Module Configuration
|
|
|
|
|
|
# Generated by BMAD installer
|
|
|
|
|
|
# Version: ${packageJson.version}
|
|
|
|
|
|
# Date: ${new Date().toISOString()}
|
|
|
|
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// For non-core modules, add core config values directly
|
|
|
|
|
|
let finalConfig = { ...config };
|
|
|
|
|
|
let coreSection = '';
|
|
|
|
|
|
|
|
|
|
|
|
if (moduleName !== 'core' && coreConfig && Object.keys(coreConfig).length > 0) {
|
|
|
|
|
|
// Add core values directly to the module config
|
|
|
|
|
|
// These will be available for reference in the module
|
|
|
|
|
|
finalConfig = {
|
|
|
|
|
|
...config,
|
|
|
|
|
|
...coreConfig, // Spread core config values directly into the module config
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Create a comment section to identify core values
|
|
|
|
|
|
coreSection = '\n# Core Configuration Values\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Convert config to YAML
|
2025-12-13 18:35:07 +08:00
|
|
|
|
let yamlContent = yaml.stringify(finalConfig, {
|
2025-09-28 23:17:07 -05:00
|
|
|
|
indent: 2,
|
2025-12-13 18:35:07 +08:00
|
|
|
|
lineWidth: 0,
|
|
|
|
|
|
minContentWidth: 0,
|
2025-09-28 23:17:07 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// If we have core values, reorganize the YAML to group them with their comment
|
|
|
|
|
|
if (coreSection && moduleName !== 'core') {
|
|
|
|
|
|
// Split the YAML into lines
|
|
|
|
|
|
const lines = yamlContent.split('\n');
|
|
|
|
|
|
const moduleConfigLines = [];
|
|
|
|
|
|
const coreConfigLines = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Separate module-specific and core config lines
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
|
const key = line.split(':')[0].trim();
|
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(coreConfig, key)) {
|
|
|
|
|
|
coreConfigLines.push(line);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
moduleConfigLines.push(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Rebuild YAML with module config first, then core config with comment
|
|
|
|
|
|
yamlContent = moduleConfigLines.join('\n');
|
|
|
|
|
|
if (coreConfigLines.length > 0) {
|
|
|
|
|
|
yamlContent += coreSection + coreConfigLines.join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 04:18:12 +02:00
|
|
|
|
// Write the clean config file with POSIX-compliant final newline
|
|
|
|
|
|
const content = header + yamlContent;
|
|
|
|
|
|
await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8');
|
2025-09-30 21:20:13 -05:00
|
|
|
|
|
|
|
|
|
|
// Track the config file in installedFiles
|
|
|
|
|
|
this.installedFiles.push(configPath);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Install core with resolved dependencies
|
|
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
|
|
|
|
|
* @param {Object} coreFiles - Core files to install
|
|
|
|
|
|
*/
|
|
|
|
|
|
async installCoreWithDependencies(bmadDir, coreFiles) {
|
|
|
|
|
|
const sourcePath = getModulePath('core');
|
|
|
|
|
|
const targetPath = path.join(bmadDir, 'core');
|
|
|
|
|
|
|
|
|
|
|
|
// Install full core
|
|
|
|
|
|
await this.installCore(bmadDir);
|
|
|
|
|
|
|
|
|
|
|
|
// If there are specific dependency files, ensure they're included
|
|
|
|
|
|
if (coreFiles) {
|
|
|
|
|
|
// Already handled by installCore for core module
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Install module with resolved dependencies
|
|
|
|
|
|
* @param {string} moduleName - Module name
|
|
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
|
|
|
|
|
* @param {Object} moduleFiles - Module files to install
|
|
|
|
|
|
*/
|
|
|
|
|
|
async installModuleWithDependencies(moduleName, bmadDir, moduleFiles) {
|
2025-10-27 22:38:34 -05:00
|
|
|
|
// Get module configuration for conditional installation
|
|
|
|
|
|
const moduleConfig = this.configCollector.collectedConfig[moduleName] || {};
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Use existing module manager for full installation with file tracking
|
|
|
|
|
|
// Note: Module-specific installers are called separately after IDE setup
|
|
|
|
|
|
await this.moduleManager.install(
|
|
|
|
|
|
moduleName,
|
|
|
|
|
|
bmadDir,
|
|
|
|
|
|
(filePath) => {
|
|
|
|
|
|
this.installedFiles.push(filePath);
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
skipModuleInstaller: true, // We'll run it later after IDE setup
|
2025-10-27 22:38:34 -05:00
|
|
|
|
moduleConfig: moduleConfig, // Pass module config for conditional filtering
|
2025-09-28 23:17:07 -05:00
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-10-02 21:45:59 -05:00
|
|
|
|
// Process agent files to build YAML agents and create customize templates
|
|
|
|
|
|
const modulePath = path.join(bmadDir, moduleName);
|
|
|
|
|
|
await this.processAgentFiles(modulePath, moduleName);
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Dependencies are already included in full module install
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Install partial module (only dependencies needed by other modules)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async installPartialModule(moduleName, bmadDir, files) {
|
|
|
|
|
|
const sourceBase = getModulePath(moduleName);
|
|
|
|
|
|
const targetBase = path.join(bmadDir, moduleName);
|
|
|
|
|
|
|
|
|
|
|
|
// Create module directory
|
|
|
|
|
|
await fs.ensureDir(targetBase);
|
|
|
|
|
|
|
|
|
|
|
|
// Copy only the required dependency files
|
|
|
|
|
|
if (files.agents && files.agents.length > 0) {
|
|
|
|
|
|
const agentsDir = path.join(targetBase, 'agents');
|
|
|
|
|
|
await fs.ensureDir(agentsDir);
|
|
|
|
|
|
|
|
|
|
|
|
for (const agentPath of files.agents) {
|
|
|
|
|
|
const fileName = path.basename(agentPath);
|
|
|
|
|
|
const sourcePath = path.join(sourceBase, 'agents', fileName);
|
|
|
|
|
|
const targetPath = path.join(agentsDir, fileName);
|
|
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(sourcePath)) {
|
2025-11-08 13:58:43 -06:00
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
this.installedFiles.push(targetPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (files.tasks && files.tasks.length > 0) {
|
|
|
|
|
|
const tasksDir = path.join(targetBase, 'tasks');
|
|
|
|
|
|
await fs.ensureDir(tasksDir);
|
|
|
|
|
|
|
|
|
|
|
|
for (const taskPath of files.tasks) {
|
|
|
|
|
|
const fileName = path.basename(taskPath);
|
|
|
|
|
|
const sourcePath = path.join(sourceBase, 'tasks', fileName);
|
|
|
|
|
|
const targetPath = path.join(tasksDir, fileName);
|
|
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(sourcePath)) {
|
2025-11-08 13:58:43 -06:00
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
this.installedFiles.push(targetPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 19:38:38 -05:00
|
|
|
|
if (files.tools && files.tools.length > 0) {
|
|
|
|
|
|
const toolsDir = path.join(targetBase, 'tools');
|
|
|
|
|
|
await fs.ensureDir(toolsDir);
|
|
|
|
|
|
|
|
|
|
|
|
for (const toolPath of files.tools) {
|
|
|
|
|
|
const fileName = path.basename(toolPath);
|
|
|
|
|
|
const sourcePath = path.join(sourceBase, 'tools', fileName);
|
|
|
|
|
|
const targetPath = path.join(toolsDir, fileName);
|
|
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(sourcePath)) {
|
2025-11-08 13:58:43 -06:00
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
2025-10-26 19:38:38 -05:00
|
|
|
|
this.installedFiles.push(targetPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
if (files.templates && files.templates.length > 0) {
|
|
|
|
|
|
const templatesDir = path.join(targetBase, 'templates');
|
|
|
|
|
|
await fs.ensureDir(templatesDir);
|
|
|
|
|
|
|
|
|
|
|
|
for (const templatePath of files.templates) {
|
|
|
|
|
|
const fileName = path.basename(templatePath);
|
|
|
|
|
|
const sourcePath = path.join(sourceBase, 'templates', fileName);
|
|
|
|
|
|
const targetPath = path.join(templatesDir, fileName);
|
|
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(sourcePath)) {
|
2025-11-08 13:58:43 -06:00
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
this.installedFiles.push(targetPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (files.data && files.data.length > 0) {
|
|
|
|
|
|
for (const dataPath of files.data) {
|
|
|
|
|
|
// Preserve directory structure for data files
|
|
|
|
|
|
const relative = path.relative(sourceBase, dataPath);
|
|
|
|
|
|
const targetPath = path.join(targetBase, relative);
|
|
|
|
|
|
|
|
|
|
|
|
await fs.ensureDir(path.dirname(targetPath));
|
|
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(dataPath)) {
|
2025-11-08 13:58:43 -06:00
|
|
|
|
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
this.installedFiles.push(targetPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create a marker file to indicate this is a partial installation
|
|
|
|
|
|
const markerPath = path.join(targetBase, '.partial');
|
|
|
|
|
|
await fs.writeFile(
|
|
|
|
|
|
markerPath,
|
|
|
|
|
|
`This module contains only dependencies required by other modules.\nInstalled: ${new Date().toISOString()}\n`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Private: Install core
|
|
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
|
|
|
|
|
*/
|
|
|
|
|
|
async installCore(bmadDir) {
|
|
|
|
|
|
const sourcePath = getModulePath('core');
|
|
|
|
|
|
const targetPath = path.join(bmadDir, 'core');
|
|
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Copy core files (skip .agent.yaml files like modules do)
|
|
|
|
|
|
await this.copyCoreFiles(sourcePath, targetPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Compile agents using the same compiler as modules
|
|
|
|
|
|
const { ModuleManager } = require('../modules/manager');
|
|
|
|
|
|
const moduleManager = new ModuleManager();
|
|
|
|
|
|
await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
|
|
|
|
|
// Process agent files to inject activation block
|
|
|
|
|
|
await this.processAgentFiles(targetPath, 'core');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-13 17:50:33 +08:00
|
|
|
|
* Copy core files (similar to copyModuleWithFiltering but for core)
|
|
|
|
|
|
* @param {string} sourcePath - Source path
|
|
|
|
|
|
* @param {string} targetPath - Target path
|
2025-09-28 23:17:07 -05:00
|
|
|
|
*/
|
2025-12-13 17:50:33 +08:00
|
|
|
|
async copyCoreFiles(sourcePath, targetPath) {
|
|
|
|
|
|
// Get all files in source
|
2025-09-28 23:17:07 -05:00
|
|
|
|
const files = await this.getFileList(sourcePath);
|
|
|
|
|
|
|
|
|
|
|
|
for (const file of files) {
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Skip sub-modules directory - these are IDE-specific and handled separately
|
|
|
|
|
|
if (file.startsWith('sub-modules/')) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Skip sidecar directories - they are handled separately during agent compilation
|
|
|
|
|
|
if (
|
|
|
|
|
|
path
|
|
|
|
|
|
.dirname(file)
|
|
|
|
|
|
.split('/')
|
|
|
|
|
|
.some((dir) => dir.toLowerCase().includes('sidecar'))
|
|
|
|
|
|
) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Skip _module-installer directory - it's only needed at install time
|
|
|
|
|
|
if (file.startsWith('_module-installer/') || file === 'module.yaml') {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Skip config.yaml templates - we'll generate clean ones with actual values
|
2025-12-13 17:50:33 +08:00
|
|
|
|
if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Skip .agent.yaml files - they will be compiled separately
|
|
|
|
|
|
if (file.endsWith('.agent.yaml')) {
|
2025-09-28 23:17:07 -05:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sourceFile = path.join(sourcePath, file);
|
|
|
|
|
|
const targetFile = path.join(targetPath, file);
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is an agent file
|
2025-12-13 17:50:33 +08:00
|
|
|
|
if (file.startsWith('agents/') && file.endsWith('.md')) {
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Read the file to check for localskip
|
|
|
|
|
|
const content = await fs.readFile(sourceFile, 'utf8');
|
|
|
|
|
|
|
|
|
|
|
|
// Check for localskip="true" in the agent tag
|
|
|
|
|
|
const agentMatch = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
|
|
|
|
|
|
if (agentMatch) {
|
|
|
|
|
|
console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`));
|
|
|
|
|
|
continue; // Skip this agent
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Check if this is a workflow.yaml file
|
|
|
|
|
|
if (file.endsWith('workflow.yaml')) {
|
|
|
|
|
|
await fs.ensureDir(path.dirname(targetFile));
|
|
|
|
|
|
await this.copyWorkflowYamlStripped(sourceFile, targetFile);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Copy the file with placeholder replacement
|
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad');
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
|
|
|
|
|
// Track the installed file
|
|
|
|
|
|
this.installedFiles.push(targetFile);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get list of all files in a directory recursively
|
|
|
|
|
|
* @param {string} dir - Directory path
|
|
|
|
|
|
* @param {string} baseDir - Base directory for relative paths
|
|
|
|
|
|
* @returns {Array} List of relative file paths
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getFileList(dir, baseDir = dir) {
|
|
|
|
|
|
const files = [];
|
|
|
|
|
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
|
const fullPath = path.join(dir, entry.name);
|
|
|
|
|
|
|
|
|
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
|
// Skip _module-installer directories
|
|
|
|
|
|
if (entry.name === '_module-installer') {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
const subFiles = await this.getFileList(fullPath, baseDir);
|
|
|
|
|
|
files.push(...subFiles);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
files.push(path.relative(baseDir, fullPath));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return files;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-02 21:45:59 -05:00
|
|
|
|
* Process agent files to build YAML agents and inject activation blocks
|
|
|
|
|
|
* @param {string} modulePath - Path to module in bmad/ installation
|
2025-09-28 23:17:07 -05:00
|
|
|
|
* @param {string} moduleName - Module name
|
|
|
|
|
|
*/
|
|
|
|
|
|
async processAgentFiles(modulePath, moduleName) {
|
|
|
|
|
|
const agentsPath = path.join(modulePath, 'agents');
|
|
|
|
|
|
|
|
|
|
|
|
// Check if agents directory exists
|
|
|
|
|
|
if (!(await fs.pathExists(agentsPath))) {
|
|
|
|
|
|
return; // No agents to process
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-02 21:45:59 -05:00
|
|
|
|
// Determine project directory (parent of bmad/ directory)
|
|
|
|
|
|
const bmadDir = path.dirname(modulePath);
|
|
|
|
|
|
const projectDir = path.dirname(bmadDir);
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
|
2025-10-02 21:45:59 -05:00
|
|
|
|
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// Ensure _config/agents directory exists
|
2025-10-02 21:45:59 -05:00
|
|
|
|
await fs.ensureDir(cfgAgentsDir);
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
// Get all agent files
|
|
|
|
|
|
const agentFiles = await fs.readdir(agentsPath);
|
|
|
|
|
|
|
|
|
|
|
|
for (const agentFile of agentFiles) {
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Skip .agent.yaml files - they should already be compiled by compileModuleAgents
|
2025-10-02 21:45:59 -05:00
|
|
|
|
if (agentFile.endsWith('.agent.yaml')) {
|
2025-12-13 17:50:33 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-10-02 21:45:59 -05:00
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Only process .md files (already compiled from YAML)
|
|
|
|
|
|
if (!agentFile.endsWith('.md')) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-10-02 21:45:59 -05:00
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
const agentName = agentFile.replace('.md', '');
|
|
|
|
|
|
const mdPath = path.join(agentsPath, agentFile);
|
|
|
|
|
|
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
|
2025-12-07 21:41:37 -06:00
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// For .md files that are already compiled, we don't need to do much
|
|
|
|
|
|
// Just ensure the customize template exists
|
|
|
|
|
|
if (!(await fs.pathExists(customizePath))) {
|
|
|
|
|
|
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
|
|
|
|
|
|
if (await fs.pathExists(genericTemplatePath)) {
|
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad');
|
|
|
|
|
|
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
|
2025-12-06 21:08:57 -06:00
|
|
|
|
}
|
2025-12-13 17:50:33 +08:00
|
|
|
|
}
|
2025-10-02 21:45:59 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-04 01:26:38 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Build standalone agents in bmad/agents/ directory
|
|
|
|
|
|
* @param {string} bmadDir - Path to bmad directory
|
|
|
|
|
|
* @param {string} projectDir - Path to project directory
|
|
|
|
|
|
*/
|
|
|
|
|
|
async buildStandaloneAgents(bmadDir, projectDir) {
|
|
|
|
|
|
const standaloneAgentsPath = path.join(bmadDir, 'agents');
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
|
2025-10-04 01:26:38 -05:00
|
|
|
|
|
|
|
|
|
|
// Check if standalone agents directory exists
|
|
|
|
|
|
if (!(await fs.pathExists(standaloneAgentsPath))) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get all subdirectories in agents/
|
|
|
|
|
|
const agentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
|
|
|
|
|
|
|
|
|
|
|
|
for (const agentDir of agentDirs) {
|
|
|
|
|
|
if (!agentDir.isDirectory()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
|
|
|
|
|
|
|
|
|
|
|
|
// Find any .agent.yaml file in the directory
|
|
|
|
|
|
const files = await fs.readdir(agentDirPath);
|
|
|
|
|
|
const yamlFile = files.find((f) => f.endsWith('.agent.yaml'));
|
|
|
|
|
|
|
|
|
|
|
|
if (!yamlFile) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const agentName = path.basename(yamlFile, '.agent.yaml');
|
|
|
|
|
|
const sourceYamlPath = path.join(agentDirPath, yamlFile);
|
|
|
|
|
|
const targetMdPath = path.join(agentDirPath, `${agentName}.md`);
|
|
|
|
|
|
const customizePath = path.join(cfgAgentsDir, `${agentName}.customize.yaml`);
|
|
|
|
|
|
|
|
|
|
|
|
// Check for customizations
|
|
|
|
|
|
const customizeExists = await fs.pathExists(customizePath);
|
|
|
|
|
|
let customizedFields = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (customizeExists) {
|
|
|
|
|
|
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
2025-12-13 18:35:07 +08:00
|
|
|
|
const yaml = require('yaml');
|
2025-12-13 17:50:33 +08:00
|
|
|
|
const customizeYaml = yaml.parse(customizeContent);
|
2025-10-04 01:26:38 -05:00
|
|
|
|
|
|
|
|
|
|
// Detect what fields are customized (similar to rebuildAgentFiles)
|
|
|
|
|
|
if (customizeYaml) {
|
|
|
|
|
|
if (customizeYaml.persona) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(customizeYaml.persona)) {
|
|
|
|
|
|
if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
|
|
|
|
|
|
customizedFields.push(`persona.${key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (customizeYaml.agent?.metadata) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
|
|
|
|
|
|
if (value !== '' && value !== null) {
|
|
|
|
|
|
customizedFields.push(`metadata.${key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
|
|
|
|
|
|
customizedFields.push('critical_actions');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (customizeYaml.menu && customizeYaml.menu.length > 0) {
|
|
|
|
|
|
customizedFields.push('menu');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build YAML to XML .md
|
2025-12-05 17:54:03 -07:00
|
|
|
|
let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
|
2025-10-04 01:26:38 -05:00
|
|
|
|
includeMetadata: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-04 21:33:19 -05:00
|
|
|
|
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
|
|
|
|
|
|
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
|
2025-10-04 01:26:38 -05:00
|
|
|
|
|
2025-12-05 17:54:03 -07:00
|
|
|
|
// Process TTS injection points (pass targetPath for tracking)
|
|
|
|
|
|
xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
|
|
|
|
|
|
|
2025-11-05 04:18:12 +02:00
|
|
|
|
// Write the built .md file with POSIX-compliant final newline
|
|
|
|
|
|
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
|
|
|
|
|
|
await fs.writeFile(targetMdPath, content, 'utf8');
|
2025-10-04 01:26:38 -05:00
|
|
|
|
|
|
|
|
|
|
// Display result
|
|
|
|
|
|
if (customizedFields.length > 0) {
|
|
|
|
|
|
console.log(chalk.dim(` Built standalone agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.dim(` Built standalone agent: ${agentName}.md`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-03 11:54:32 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Rebuild agent files from installer source (for compile command)
|
|
|
|
|
|
* @param {string} modulePath - Path to module in bmad/ installation
|
|
|
|
|
|
* @param {string} moduleName - Module name
|
|
|
|
|
|
*/
|
|
|
|
|
|
async rebuildAgentFiles(modulePath, moduleName) {
|
|
|
|
|
|
// Get source agents directory from installer
|
|
|
|
|
|
const sourceAgentsPath =
|
|
|
|
|
|
moduleName === 'core' ? path.join(getModulePath('core'), 'agents') : path.join(getSourcePath(`modules/${moduleName}`), 'agents');
|
|
|
|
|
|
|
|
|
|
|
|
if (!(await fs.pathExists(sourceAgentsPath))) {
|
|
|
|
|
|
return; // No source agents to rebuild
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Determine project directory (parent of bmad/ directory)
|
|
|
|
|
|
const bmadDir = path.dirname(modulePath);
|
|
|
|
|
|
const projectDir = path.dirname(bmadDir);
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
|
2025-10-03 11:54:32 -05:00
|
|
|
|
const targetAgentsPath = path.join(modulePath, 'agents');
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure target directory exists
|
|
|
|
|
|
await fs.ensureDir(targetAgentsPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Get all YAML agent files from source
|
|
|
|
|
|
const sourceFiles = await fs.readdir(sourceAgentsPath);
|
|
|
|
|
|
|
|
|
|
|
|
for (const file of sourceFiles) {
|
|
|
|
|
|
if (file.endsWith('.agent.yaml')) {
|
|
|
|
|
|
const agentName = file.replace('.agent.yaml', '');
|
|
|
|
|
|
const sourceYamlPath = path.join(sourceAgentsPath, file);
|
|
|
|
|
|
const targetMdPath = path.join(targetAgentsPath, `${agentName}.md`);
|
|
|
|
|
|
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
|
|
|
|
|
|
|
|
|
|
|
|
// Check for customizations
|
|
|
|
|
|
const customizeExists = await fs.pathExists(customizePath);
|
|
|
|
|
|
let customizedFields = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (customizeExists) {
|
|
|
|
|
|
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
2025-12-13 18:35:07 +08:00
|
|
|
|
const yaml = require('yaml');
|
|
|
|
|
|
const customizeYaml = yaml.parse(customizeContent);
|
2025-10-03 11:54:32 -05:00
|
|
|
|
|
|
|
|
|
|
// Detect what fields are customized
|
|
|
|
|
|
if (customizeYaml) {
|
|
|
|
|
|
if (customizeYaml.persona) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(customizeYaml.persona)) {
|
|
|
|
|
|
if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
|
|
|
|
|
|
customizedFields.push(`persona.${key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (customizeYaml.agent?.metadata) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
|
|
|
|
|
|
if (value !== '' && value !== null) {
|
|
|
|
|
|
customizedFields.push(`metadata.${key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
|
|
|
|
|
|
customizedFields.push('critical_actions');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (customizeYaml.memories && customizeYaml.memories.length > 0) {
|
|
|
|
|
|
customizedFields.push('memories');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (customizeYaml.menu && customizeYaml.menu.length > 0) {
|
|
|
|
|
|
customizedFields.push('menu');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (customizeYaml.prompts && customizeYaml.prompts.length > 0) {
|
|
|
|
|
|
customizedFields.push('prompts');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Read the YAML content
|
|
|
|
|
|
const yamlContent = await fs.readFile(sourceYamlPath, 'utf8');
|
2025-10-03 11:54:32 -05:00
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Read customize content if exists
|
|
|
|
|
|
let customizeData = {};
|
|
|
|
|
|
if (customizeExists) {
|
|
|
|
|
|
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
|
|
|
|
|
const yaml = require('yaml');
|
|
|
|
|
|
customizeData = yaml.parse(customizeContent);
|
|
|
|
|
|
}
|
2025-10-03 11:54:32 -05:00
|
|
|
|
|
2025-12-13 18:35:07 +08:00
|
|
|
|
// Build agent answers from customize data (filter empty values)
|
2025-12-13 17:50:33 +08:00
|
|
|
|
const answers = {};
|
|
|
|
|
|
if (customizeData.persona) {
|
2025-12-13 18:35:07 +08:00
|
|
|
|
Object.assign(answers, filterCustomizationData(customizeData.persona));
|
2025-12-13 17:50:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (customizeData.agent?.metadata) {
|
2025-12-13 18:35:07 +08:00
|
|
|
|
const filteredMetadata = filterCustomizationData(customizeData.agent.metadata);
|
|
|
|
|
|
if (Object.keys(filteredMetadata).length > 0) {
|
|
|
|
|
|
Object.assign(answers, { metadata: filteredMetadata });
|
|
|
|
|
|
}
|
2025-12-13 17:50:33 +08:00
|
|
|
|
}
|
2025-12-13 18:35:07 +08:00
|
|
|
|
if (customizeData.critical_actions && customizeData.critical_actions.length > 0) {
|
2025-12-13 17:50:33 +08:00
|
|
|
|
answers.critical_actions = customizeData.critical_actions;
|
|
|
|
|
|
}
|
2025-12-13 18:35:07 +08:00
|
|
|
|
if (customizeData.memories && customizeData.memories.length > 0) {
|
2025-12-13 17:50:33 +08:00
|
|
|
|
answers.memories = customizeData.memories;
|
|
|
|
|
|
}
|
2025-12-06 21:08:57 -06:00
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Get core config for agent_sidecar_folder
|
|
|
|
|
|
const coreConfigPath = path.join(bmadDir, 'bmb', 'config.yaml');
|
|
|
|
|
|
let coreConfig = {};
|
2025-12-06 21:08:57 -06:00
|
|
|
|
if (await fs.pathExists(coreConfigPath)) {
|
2025-12-13 17:50:33 +08:00
|
|
|
|
const yaml = require('yaml');
|
2025-12-06 21:08:57 -06:00
|
|
|
|
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
|
2025-12-13 17:50:33 +08:00
|
|
|
|
coreConfig = yaml.parse(coreConfigContent);
|
2025-12-06 21:08:57 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Compile using the same compiler as initial installation
|
|
|
|
|
|
const { compileAgent } = require('../../../lib/agent/compiler');
|
|
|
|
|
|
const { xml } = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
|
|
|
|
|
|
config: coreConfig,
|
|
|
|
|
|
});
|
2025-12-06 21:08:57 -06:00
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Replace _bmad with actual folder name if needed
|
|
|
|
|
|
const finalXml = xml.replaceAll('_bmad', path.basename(bmadDir));
|
2025-12-05 17:54:03 -07:00
|
|
|
|
|
2025-11-05 04:18:12 +02:00
|
|
|
|
// Write the rebuilt .md file with POSIX-compliant final newline
|
2025-12-13 17:50:33 +08:00
|
|
|
|
const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n';
|
2025-11-05 04:18:12 +02:00
|
|
|
|
await fs.writeFile(targetMdPath, content, 'utf8');
|
2025-10-03 11:54:32 -05:00
|
|
|
|
|
|
|
|
|
|
// Display result with customizations if any
|
|
|
|
|
|
if (customizedFields.length > 0) {
|
|
|
|
|
|
console.log(chalk.dim(` Rebuilt agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.dim(` Rebuilt agent: ${agentName}.md`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-02 21:45:59 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Compile/rebuild all agents and tasks for quick updates
|
|
|
|
|
|
* @param {Object} config - Compilation configuration
|
|
|
|
|
|
* @returns {Object} Compilation results
|
|
|
|
|
|
*/
|
|
|
|
|
|
async compileAgents(config) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const projectDir = path.resolve(config.directory);
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const { bmadDir } = await this.findBmadDir(projectDir);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-10-02 21:45:59 -05:00
|
|
|
|
// Check if bmad directory exists
|
|
|
|
|
|
if (!(await fs.pathExists(bmadDir))) {
|
|
|
|
|
|
throw new Error(`BMAD not installed at ${bmadDir}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-13 17:50:33 +08:00
|
|
|
|
// Get installed modules from manifest
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
|
2025-12-13 17:50:33 +08:00
|
|
|
|
let installedModules = [];
|
|
|
|
|
|
let manifest = null;
|
|
|
|
|
|
if (await fs.pathExists(manifestPath)) {
|
|
|
|
|
|
const manifestContent = await fs.readFile(manifestPath, 'utf8');
|
2025-12-13 18:35:07 +08:00
|
|
|
|
const yaml = require('yaml');
|
|
|
|
|
|
manifest = yaml.parse(manifestContent);
|
2025-12-13 17:50:33 +08:00
|
|
|
|
installedModules = manifest.modules || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 20:46:09 -06:00
|
|
|
|
// Check for custom modules with missing sources
|
|
|
|
|
|
if (manifest && manifest.customModules && manifest.customModules.length > 0) {
|
|
|
|
|
|
console.log(chalk.yellow('\nChecking custom module sources before compilation...'));
|
|
|
|
|
|
|
|
|
|
|
|
const customModuleSources = new Map();
|
|
|
|
|
|
for (const customModule of manifest.customModules) {
|
|
|
|
|
|
customModuleSources.set(customModule.id, customModule);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const projectRoot = getProjectRoot();
|
|
|
|
|
|
await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-02 21:45:59 -05:00
|
|
|
|
let agentCount = 0;
|
|
|
|
|
|
let taskCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Process all modules in bmad directory
|
|
|
|
|
|
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
2025-12-13 19:41:09 +08:00
|
|
|
|
if (entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs') {
|
2025-10-02 21:45:59 -05:00
|
|
|
|
const modulePath = path.join(bmadDir, entry.name);
|
|
|
|
|
|
|
2025-10-04 01:26:38 -05:00
|
|
|
|
// Special handling for standalone agents in bmad/agents/ directory
|
|
|
|
|
|
if (entry.name === 'agents') {
|
|
|
|
|
|
await this.buildStandaloneAgents(bmadDir, projectDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Count standalone agents
|
|
|
|
|
|
const standaloneAgentsPath = path.join(bmadDir, 'agents');
|
|
|
|
|
|
const standaloneAgentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
|
|
|
|
|
|
for (const agentDir of standaloneAgentDirs) {
|
|
|
|
|
|
if (agentDir.isDirectory()) {
|
|
|
|
|
|
const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
|
|
|
|
|
|
const agentFiles = await fs.readdir(agentDirPath);
|
|
|
|
|
|
agentCount += agentFiles.filter((f) => f.endsWith('.md') && !f.endsWith('.agent.yaml')).length;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Rebuild module agents from installer source
|
|
|
|
|
|
const agentsPath = path.join(modulePath, 'agents');
|
|
|
|
|
|
if (await fs.pathExists(agentsPath)) {
|
|
|
|
|
|
await this.rebuildAgentFiles(modulePath, entry.name);
|
|
|
|
|
|
const agentFiles = await fs.readdir(agentsPath);
|
|
|
|
|
|
agentCount += agentFiles.filter((f) => f.endsWith('.md')).length;
|
|
|
|
|
|
}
|
2025-10-02 21:45:59 -05:00
|
|
|
|
|
2025-10-04 01:26:38 -05:00
|
|
|
|
// Count tasks (already built)
|
|
|
|
|
|
const tasksPath = path.join(modulePath, 'tasks');
|
|
|
|
|
|
if (await fs.pathExists(tasksPath)) {
|
|
|
|
|
|
const taskFiles = await fs.readdir(tasksPath);
|
|
|
|
|
|
taskCount += taskFiles.filter((f) => f.endsWith('.md')).length;
|
|
|
|
|
|
}
|
2025-10-02 21:45:59 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-12 22:40:45 -06:00
|
|
|
|
// Update IDE configurations using the existing IDE list from manifest
|
2025-12-13 17:50:33 +08:00
|
|
|
|
if (manifest && manifest.ides && manifest.ides.length > 0) {
|
|
|
|
|
|
for (const ide of manifest.ides) {
|
2025-10-02 21:45:59 -05:00
|
|
|
|
await this.ideManager.setup(ide, projectDir, bmadDir, {
|
2025-10-09 23:07:12 -05:00
|
|
|
|
selectedModules: installedModules,
|
2025-10-02 21:45:59 -05:00
|
|
|
|
skipModuleInstall: true, // Skip module installation, just update IDE files
|
|
|
|
|
|
verbose: config.verbose,
|
2025-11-12 22:40:45 -06:00
|
|
|
|
preCollectedConfig: { _alreadyConfigured: true }, // Skip all interactive prompts during compile
|
2025-10-02 21:45:59 -05:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-11-12 22:40:45 -06:00
|
|
|
|
console.log(chalk.green('✓ IDE configurations updated'));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.yellow('⚠️ No IDEs configured. Skipping IDE update.'));
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
2025-10-02 21:45:59 -05:00
|
|
|
|
return { agentCount, taskCount };
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
throw error;
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Private: Update core
|
|
|
|
|
|
*/
|
|
|
|
|
|
async updateCore(bmadDir, force = false) {
|
|
|
|
|
|
const sourcePath = getModulePath('core');
|
|
|
|
|
|
const targetPath = path.join(bmadDir, 'core');
|
|
|
|
|
|
|
|
|
|
|
|
if (force) {
|
|
|
|
|
|
await fs.remove(targetPath);
|
|
|
|
|
|
await this.installCore(bmadDir);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Selective update - preserve user modifications
|
|
|
|
|
|
await this.fileOps.syncDirectory(sourcePath, targetPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 16:17:37 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Quick update method - preserves all settings and only prompts for new config fields
|
|
|
|
|
|
* @param {Object} config - Configuration with directory
|
|
|
|
|
|
* @returns {Object} Update result
|
|
|
|
|
|
*/
|
|
|
|
|
|
async quickUpdate(config) {
|
|
|
|
|
|
const ora = require('ora');
|
|
|
|
|
|
const spinner = ora('Starting quick update...').start();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const projectDir = path.resolve(config.directory);
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const { bmadDir } = await this.findBmadDir(projectDir);
|
2025-10-26 16:17:37 -05:00
|
|
|
|
|
|
|
|
|
|
// Check if bmad directory exists
|
|
|
|
|
|
if (!(await fs.pathExists(bmadDir))) {
|
|
|
|
|
|
spinner.fail('No BMAD installation found');
|
|
|
|
|
|
throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
spinner.text = 'Detecting installed modules and configuration...';
|
|
|
|
|
|
|
|
|
|
|
|
// Detect existing installation
|
|
|
|
|
|
const existingInstall = await this.detector.detect(bmadDir);
|
|
|
|
|
|
const installedModules = existingInstall.modules.map((m) => m.id);
|
|
|
|
|
|
const configuredIdes = existingInstall.ides || [];
|
2025-12-07 20:46:09 -06:00
|
|
|
|
const projectRoot = path.dirname(bmadDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Get custom module sources from manifest
|
|
|
|
|
|
const customModuleSources = new Map();
|
|
|
|
|
|
if (existingInstall.customModules) {
|
|
|
|
|
|
for (const customModule of existingInstall.customModules) {
|
|
|
|
|
|
// Ensure we have an absolute sourcePath
|
|
|
|
|
|
let absoluteSourcePath = customModule.sourcePath;
|
|
|
|
|
|
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// Check if sourcePath is a cache-relative path (starts with _config/)
|
|
|
|
|
|
if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
|
2025-12-07 20:46:09 -06:00
|
|
|
|
// Convert cache-relative path to absolute path
|
|
|
|
|
|
absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
// If no sourcePath but we have relativePath, convert it
|
|
|
|
|
|
else if (!absoluteSourcePath && customModule.relativePath) {
|
|
|
|
|
|
// relativePath is relative to the project root (parent of bmad dir)
|
|
|
|
|
|
absoluteSourcePath = path.resolve(projectRoot, customModule.relativePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Ensure sourcePath is absolute for anything else
|
|
|
|
|
|
else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
|
|
|
|
|
|
absoluteSourcePath = path.resolve(absoluteSourcePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update the custom module object with the absolute path
|
|
|
|
|
|
const updatedModule = {
|
|
|
|
|
|
...customModule,
|
|
|
|
|
|
sourcePath: absoluteSourcePath,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
customModuleSources.set(customModule.id, updatedModule);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-26 16:17:37 -05:00
|
|
|
|
|
2025-10-26 17:04:27 -05:00
|
|
|
|
// Load saved IDE configurations
|
|
|
|
|
|
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
|
|
|
|
|
|
2025-10-26 16:17:37 -05:00
|
|
|
|
// Get available modules (what we have source for)
|
2025-12-07 20:46:09 -06:00
|
|
|
|
const availableModulesData = await this.moduleManager.listAvailable();
|
|
|
|
|
|
const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
|
|
|
|
|
|
|
|
|
|
|
|
// Add custom modules from manifest if their sources exist
|
|
|
|
|
|
for (const [moduleId, customModule] of customModuleSources) {
|
|
|
|
|
|
// Use the absolute sourcePath
|
|
|
|
|
|
const sourcePath = customModule.sourcePath;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if source exists at the recorded path
|
|
|
|
|
|
if (
|
|
|
|
|
|
sourcePath &&
|
|
|
|
|
|
(await fs.pathExists(sourcePath)) && // Add to available modules if not already there
|
|
|
|
|
|
!availableModules.some((m) => m.id === moduleId)
|
|
|
|
|
|
) {
|
|
|
|
|
|
availableModules.push({
|
|
|
|
|
|
id: moduleId,
|
|
|
|
|
|
name: customModule.name || moduleId,
|
|
|
|
|
|
path: sourcePath,
|
|
|
|
|
|
isCustom: true,
|
|
|
|
|
|
fromManifest: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check for untracked custom modules (installed but not in manifest)
|
|
|
|
|
|
const untrackedCustomModules = [];
|
|
|
|
|
|
for (const installedModule of installedModules) {
|
|
|
|
|
|
// Skip standard modules and core
|
|
|
|
|
|
const standardModuleIds = ['bmb', 'bmgd', 'bmm', 'cis', 'core'];
|
|
|
|
|
|
if (standardModuleIds.includes(installedModule)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this installed module is not tracked in customModules
|
|
|
|
|
|
if (!customModuleSources.has(installedModule)) {
|
|
|
|
|
|
const modulePath = path.join(bmadDir, installedModule);
|
|
|
|
|
|
if (await fs.pathExists(modulePath)) {
|
|
|
|
|
|
untrackedCustomModules.push({
|
|
|
|
|
|
id: installedModule,
|
|
|
|
|
|
name: installedModule, // We don't have the original name
|
|
|
|
|
|
path: modulePath,
|
|
|
|
|
|
untracked: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If we found untracked custom modules, offer to track them
|
|
|
|
|
|
if (untrackedCustomModules.length > 0) {
|
|
|
|
|
|
spinner.stop();
|
|
|
|
|
|
console.log(chalk.yellow(`\n⚠️ Found ${untrackedCustomModules.length} custom module(s) not tracked in manifest:`));
|
|
|
|
|
|
|
|
|
|
|
|
for (const untracked of untrackedCustomModules) {
|
|
|
|
|
|
console.log(chalk.dim(` • ${untracked.id} (installed at ${path.relative(projectRoot, untracked.path)})`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { trackModules } = await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'confirm',
|
|
|
|
|
|
name: 'trackModules',
|
|
|
|
|
|
message: chalk.cyan('Would you like to scan for their source locations?'),
|
|
|
|
|
|
default: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
if (trackModules) {
|
|
|
|
|
|
const { scanDirectory } = await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'input',
|
|
|
|
|
|
name: 'scanDirectory',
|
|
|
|
|
|
message: 'Enter directory to scan for custom module sources (or leave blank to skip):',
|
|
|
|
|
|
default: projectRoot,
|
|
|
|
|
|
validate: async (input) => {
|
|
|
|
|
|
if (input && input.trim() !== '') {
|
|
|
|
|
|
const expandedPath = path.resolve(input.trim());
|
|
|
|
|
|
if (!(await fs.pathExists(expandedPath))) {
|
|
|
|
|
|
return 'Directory does not exist';
|
|
|
|
|
|
}
|
|
|
|
|
|
const stats = await fs.stat(expandedPath);
|
|
|
|
|
|
if (!stats.isDirectory()) {
|
|
|
|
|
|
return 'Path must be a directory';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
if (scanDirectory && scanDirectory.trim() !== '') {
|
|
|
|
|
|
console.log(chalk.dim('\nScanning for custom module sources...'));
|
|
|
|
|
|
|
|
|
|
|
|
// Scan for all module.yaml files
|
|
|
|
|
|
const allModulePaths = await this.moduleManager.findModulesInProject(scanDirectory);
|
|
|
|
|
|
const { ModuleManager } = require('../modules/manager');
|
|
|
|
|
|
const mm = new ModuleManager({ scanProjectForModules: true });
|
|
|
|
|
|
|
|
|
|
|
|
for (const untracked of untrackedCustomModules) {
|
|
|
|
|
|
let foundSource = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Try to find by module ID
|
|
|
|
|
|
for (const modulePath of allModulePaths) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const moduleInfo = await mm.getModuleInfo(modulePath);
|
|
|
|
|
|
if (moduleInfo && moduleInfo.id === untracked.id) {
|
|
|
|
|
|
foundSource = {
|
|
|
|
|
|
path: modulePath,
|
|
|
|
|
|
info: moduleInfo,
|
|
|
|
|
|
};
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Continue searching
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (foundSource) {
|
|
|
|
|
|
console.log(chalk.green(` ✓ Found source for ${untracked.id}: ${path.relative(projectRoot, foundSource.path)}`));
|
|
|
|
|
|
|
|
|
|
|
|
// Add to manifest
|
|
|
|
|
|
await this.manifest.addCustomModule(bmadDir, {
|
|
|
|
|
|
id: untracked.id,
|
|
|
|
|
|
name: foundSource.info.name || untracked.name,
|
|
|
|
|
|
sourcePath: path.resolve(foundSource.path),
|
|
|
|
|
|
installDate: new Date().toISOString(),
|
|
|
|
|
|
tracked: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Add to customModuleSources for processing
|
|
|
|
|
|
customModuleSources.set(untracked.id, {
|
|
|
|
|
|
id: untracked.id,
|
|
|
|
|
|
name: foundSource.info.name || untracked.name,
|
|
|
|
|
|
sourcePath: path.resolve(foundSource.path),
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.yellow(` ⚠ Could not find source for ${untracked.id}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.dim('\nUntracked custom modules will remain installed but cannot be updated without their source.'));
|
|
|
|
|
|
spinner.start('Preparing update...');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle missing custom module sources using shared method
|
|
|
|
|
|
const customModuleResult = await this.handleMissingCustomSources(
|
|
|
|
|
|
customModuleSources,
|
|
|
|
|
|
bmadDir,
|
|
|
|
|
|
projectRoot,
|
|
|
|
|
|
'update',
|
|
|
|
|
|
installedModules,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-12-08 12:24:30 -07:00
|
|
|
|
const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
|
2025-12-07 20:46:09 -06:00
|
|
|
|
|
|
|
|
|
|
const customModulesFromManifest = validCustomModules.map((m) => ({
|
|
|
|
|
|
...m,
|
|
|
|
|
|
isCustom: true,
|
|
|
|
|
|
hasUpdate: true,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// Add untracked modules to the update list but mark them as untrackable
|
|
|
|
|
|
for (const untracked of untrackedCustomModules) {
|
|
|
|
|
|
if (!customModuleSources.has(untracked.id)) {
|
|
|
|
|
|
customModulesFromManifest.push({
|
|
|
|
|
|
...untracked,
|
|
|
|
|
|
isCustom: true,
|
|
|
|
|
|
hasUpdate: false, // Can't update without source
|
|
|
|
|
|
untracked: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const allAvailableModules = [...availableModules, ...customModulesFromManifest];
|
|
|
|
|
|
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
|
|
|
|
|
|
|
|
|
|
|
|
// Core module is special - never include it in update flow
|
|
|
|
|
|
const nonCoreInstalledModules = installedModules.filter((id) => id !== 'core');
|
2025-10-26 16:17:37 -05:00
|
|
|
|
|
|
|
|
|
|
// Only update modules that are BOTH installed AND available (we have source for)
|
2025-12-07 20:46:09 -06:00
|
|
|
|
const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id));
|
|
|
|
|
|
const skippedModules = nonCoreInstalledModules.filter((id) => !availableModuleIds.has(id));
|
|
|
|
|
|
|
|
|
|
|
|
// Add custom modules that were kept without sources to the skipped modules
|
|
|
|
|
|
// This ensures their agents are preserved in the manifest
|
|
|
|
|
|
for (const keptModule of keptModulesWithoutSources) {
|
|
|
|
|
|
if (!skippedModules.includes(keptModule)) {
|
|
|
|
|
|
skippedModules.push(keptModule);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-26 16:17:37 -05:00
|
|
|
|
|
|
|
|
|
|
spinner.succeed(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`);
|
|
|
|
|
|
|
|
|
|
|
|
if (skippedModules.length > 0) {
|
|
|
|
|
|
console.log(chalk.yellow(`⚠️ Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Load existing configs and collect new fields (if any)
|
|
|
|
|
|
console.log(chalk.cyan('\n📋 Checking for new configuration options...'));
|
|
|
|
|
|
await this.configCollector.loadExistingConfig(projectDir);
|
|
|
|
|
|
|
|
|
|
|
|
let promptedForNewFields = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Check core config for new fields
|
|
|
|
|
|
const corePrompted = await this.configCollector.collectModuleConfigQuick('core', projectDir, true);
|
|
|
|
|
|
if (corePrompted) {
|
|
|
|
|
|
promptedForNewFields = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check each module we're updating for new fields (NOT skipped modules)
|
|
|
|
|
|
for (const moduleName of modulesToUpdate) {
|
|
|
|
|
|
const modulePrompted = await this.configCollector.collectModuleConfigQuick(moduleName, projectDir, true);
|
|
|
|
|
|
if (modulePrompted) {
|
|
|
|
|
|
promptedForNewFields = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!promptedForNewFields) {
|
|
|
|
|
|
console.log(chalk.green('✓ All configuration is up to date, no new options to configure'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add metadata
|
|
|
|
|
|
this.configCollector.collectedConfig._meta = {
|
|
|
|
|
|
version: require(path.join(getProjectRoot(), 'package.json')).version,
|
|
|
|
|
|
installDate: new Date().toISOString(),
|
|
|
|
|
|
lastModified: new Date().toISOString(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Build the config object for the installer
|
|
|
|
|
|
const installConfig = {
|
|
|
|
|
|
directory: projectDir,
|
|
|
|
|
|
installCore: true,
|
|
|
|
|
|
modules: modulesToUpdate, // Only update modules we have source for
|
|
|
|
|
|
ides: configuredIdes,
|
|
|
|
|
|
skipIde: configuredIdes.length === 0,
|
|
|
|
|
|
coreConfig: this.configCollector.collectedConfig.core,
|
|
|
|
|
|
actionType: 'install', // Use regular install flow
|
|
|
|
|
|
_quickUpdate: true, // Flag to skip certain prompts
|
|
|
|
|
|
_preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
|
2025-10-26 17:04:27 -05:00
|
|
|
|
_savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer
|
2025-12-07 20:46:09 -06:00
|
|
|
|
_customModuleSources: customModuleSources, // Pass custom module sources for updates
|
|
|
|
|
|
_existingModules: installedModules, // Pass all installed modules for manifest generation
|
2025-10-26 16:17:37 -05:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Call the standard install method
|
|
|
|
|
|
const result = await this.install(installConfig);
|
|
|
|
|
|
|
2025-11-13 18:58:50 -06:00
|
|
|
|
// Only succeed the spinner if it's still spinning
|
|
|
|
|
|
// (install method might have stopped it if folder name changed)
|
|
|
|
|
|
if (spinner.isSpinning) {
|
|
|
|
|
|
spinner.succeed('Quick update complete!');
|
|
|
|
|
|
}
|
2025-10-26 16:17:37 -05:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
moduleCount: modulesToUpdate.length + 1, // +1 for core
|
|
|
|
|
|
hadNewFields: promptedForNewFields,
|
|
|
|
|
|
modules: ['core', ...modulesToUpdate],
|
|
|
|
|
|
skippedModules: skippedModules,
|
|
|
|
|
|
ides: configuredIdes,
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
spinner.fail('Quick update failed');
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Private: Prompt for update action
|
|
|
|
|
|
*/
|
|
|
|
|
|
async promptUpdateAction() {
|
|
|
|
|
|
const inquirer = require('inquirer');
|
|
|
|
|
|
return await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'list',
|
|
|
|
|
|
name: 'action',
|
|
|
|
|
|
message: 'What would you like to do?',
|
|
|
|
|
|
choices: [
|
|
|
|
|
|
{ name: 'Update existing installation', value: 'update' },
|
|
|
|
|
|
{ name: 'Remove and reinstall', value: 'reinstall' },
|
|
|
|
|
|
{ name: 'Cancel', value: 'cancel' },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-30 19:42:12 -05:00
|
|
|
|
* Handle legacy BMAD v4 migration with automatic backup
|
|
|
|
|
|
* @param {string} projectDir - Project directory
|
2025-09-28 23:17:07 -05:00
|
|
|
|
* @param {Object} legacyV4 - Legacy V4 detection result with offenders array
|
|
|
|
|
|
*/
|
2025-09-30 19:42:12 -05:00
|
|
|
|
async handleLegacyV4Migration(projectDir, legacyV4) {
|
|
|
|
|
|
console.log(chalk.yellow.bold('\n⚠️ Legacy BMAD v4 detected'));
|
|
|
|
|
|
console.log(chalk.dim('The installer found legacy artefacts in your project.\n'));
|
|
|
|
|
|
|
2025-12-13 16:22:34 +08:00
|
|
|
|
// Separate _bmad* folders (auto-backup) from other offending paths (manual cleanup)
|
2025-09-30 19:42:12 -05:00
|
|
|
|
const bmadFolders = legacyV4.offenders.filter((p) => {
|
|
|
|
|
|
const name = path.basename(p);
|
2025-12-13 16:22:34 +08:00
|
|
|
|
return name.startsWith('_bmad'); // Only dot-prefixed folders get auto-backed up
|
2025-09-30 19:42:12 -05:00
|
|
|
|
});
|
|
|
|
|
|
const otherOffenders = legacyV4.offenders.filter((p) => {
|
|
|
|
|
|
const name = path.basename(p);
|
2025-12-13 16:22:34 +08:00
|
|
|
|
return !name.startsWith('_bmad'); // Everything else is manual cleanup
|
2025-09-30 19:42:12 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const inquirer = require('inquirer');
|
|
|
|
|
|
|
|
|
|
|
|
// Show warning for other offending paths FIRST
|
|
|
|
|
|
if (otherOffenders.length > 0) {
|
|
|
|
|
|
console.log(chalk.yellow('⚠️ Recommended cleanup:'));
|
|
|
|
|
|
console.log(chalk.dim('It is recommended to remove the following items before proceeding:\n'));
|
|
|
|
|
|
for (const p of otherOffenders) console.log(chalk.dim(` - ${p}`));
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.cyan('\nCleanup commands you can copy/paste:'));
|
|
|
|
|
|
console.log(chalk.dim('macOS/Linux:'));
|
|
|
|
|
|
for (const p of otherOffenders) console.log(chalk.dim(` rm -rf '${p}'`));
|
|
|
|
|
|
console.log(chalk.dim('Windows:'));
|
|
|
|
|
|
for (const p of otherOffenders) console.log(chalk.dim(` rmdir /S /Q "${p}"`));
|
|
|
|
|
|
|
|
|
|
|
|
const { cleanedUp } = await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'confirm',
|
|
|
|
|
|
name: 'cleanedUp',
|
|
|
|
|
|
message: 'Have you completed the recommended cleanup? (You can proceed without it, but it is recommended)',
|
|
|
|
|
|
default: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
if (cleanedUp) {
|
|
|
|
|
|
console.log(chalk.green('✓ Cleanup acknowledged\n'));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.yellow('⚠️ Proceeding without recommended cleanup\n'));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-12-13 16:22:34 +08:00
|
|
|
|
// Handle _bmad* folders with automatic backup
|
2025-09-30 19:42:12 -05:00
|
|
|
|
if (bmadFolders.length > 0) {
|
|
|
|
|
|
console.log(chalk.cyan('The following legacy folders will be moved to v4-backup:'));
|
|
|
|
|
|
for (const p of bmadFolders) console.log(chalk.dim(` - ${p}`));
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-09-30 19:42:12 -05:00
|
|
|
|
const { proceed } = await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'confirm',
|
|
|
|
|
|
name: 'proceed',
|
|
|
|
|
|
message: 'Proceed with backing up legacy v4 folders?',
|
|
|
|
|
|
default: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-09-30 19:42:12 -05:00
|
|
|
|
if (proceed) {
|
|
|
|
|
|
const backupDir = path.join(projectDir, 'v4-backup');
|
|
|
|
|
|
await fs.ensureDir(backupDir);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-09-30 19:42:12 -05:00
|
|
|
|
for (const folder of bmadFolders) {
|
|
|
|
|
|
const folderName = path.basename(folder);
|
|
|
|
|
|
const backupPath = path.join(backupDir, folderName);
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-09-30 19:42:12 -05:00
|
|
|
|
// If backup already exists, add timestamp
|
|
|
|
|
|
let finalBackupPath = backupPath;
|
|
|
|
|
|
if (await fs.pathExists(backupPath)) {
|
|
|
|
|
|
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-').split('T')[0];
|
|
|
|
|
|
finalBackupPath = path.join(backupDir, `${folderName}-${timestamp}`);
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
|
2025-09-30 19:42:12 -05:00
|
|
|
|
await fs.move(folder, finalBackupPath, { overwrite: false });
|
|
|
|
|
|
console.log(chalk.green(`✓ Moved ${folderName} to ${path.relative(projectDir, finalBackupPath)}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error('Installation cancelled by user');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 20:06:02 -05:00
|
|
|
|
/**
|
2025-09-30 21:20:13 -05:00
|
|
|
|
* Read files-manifest.csv
|
2025-09-30 20:06:02 -05:00
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
2025-09-30 21:20:13 -05:00
|
|
|
|
* @returns {Array} Array of file entries from files-manifest.csv
|
2025-09-30 20:06:02 -05:00
|
|
|
|
*/
|
2025-09-30 21:20:13 -05:00
|
|
|
|
async readFilesManifest(bmadDir) {
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const filesManifestPath = path.join(bmadDir, '_config', 'files-manifest.csv');
|
2025-09-30 21:20:13 -05:00
|
|
|
|
if (!(await fs.pathExists(filesManifestPath))) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
2025-09-30 20:06:02 -05:00
|
|
|
|
|
2025-09-30 21:20:13 -05:00
|
|
|
|
try {
|
|
|
|
|
|
const content = await fs.readFile(filesManifestPath, 'utf8');
|
|
|
|
|
|
const lines = content.split('\n');
|
|
|
|
|
|
const files = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 1; i < lines.length; i++) {
|
|
|
|
|
|
// Skip header
|
|
|
|
|
|
const line = lines[i].trim();
|
|
|
|
|
|
if (!line) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Parse CSV line properly handling quoted values
|
|
|
|
|
|
const parts = [];
|
|
|
|
|
|
let current = '';
|
|
|
|
|
|
let inQuotes = false;
|
|
|
|
|
|
|
|
|
|
|
|
for (const char of line) {
|
|
|
|
|
|
if (char === '"') {
|
|
|
|
|
|
inQuotes = !inQuotes;
|
|
|
|
|
|
} else if (char === ',' && !inQuotes) {
|
|
|
|
|
|
parts.push(current);
|
|
|
|
|
|
current = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
current += char;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
parts.push(current); // Add last part
|
|
|
|
|
|
|
|
|
|
|
|
if (parts.length >= 4) {
|
|
|
|
|
|
files.push({
|
|
|
|
|
|
type: parts[0],
|
|
|
|
|
|
name: parts[1],
|
|
|
|
|
|
module: parts[2],
|
|
|
|
|
|
path: parts[3],
|
|
|
|
|
|
hash: parts[4] || null, // Hash may not exist in old manifests
|
|
|
|
|
|
});
|
2025-09-30 20:06:02 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-30 21:20:13 -05:00
|
|
|
|
|
|
|
|
|
|
return files;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('Warning: Could not read files-manifest.csv:', error.message);
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Detect custom and modified files
|
|
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
|
|
|
|
|
* @param {Array} existingFilesManifest - Previous files from files-manifest.csv
|
|
|
|
|
|
* @returns {Object} Object with customFiles and modifiedFiles arrays
|
|
|
|
|
|
*/
|
|
|
|
|
|
async detectCustomFiles(bmadDir, existingFilesManifest) {
|
|
|
|
|
|
const customFiles = [];
|
|
|
|
|
|
const modifiedFiles = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Check if the manifest has hashes - if not, we can't detect modifications
|
|
|
|
|
|
let manifestHasHashes = false;
|
|
|
|
|
|
if (existingFilesManifest && existingFilesManifest.length > 0) {
|
|
|
|
|
|
manifestHasHashes = existingFilesManifest.some((f) => f.hash);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build map of previously installed files from files-manifest.csv with their hashes
|
|
|
|
|
|
const installedFilesMap = new Map();
|
|
|
|
|
|
for (const fileEntry of existingFilesManifest) {
|
|
|
|
|
|
if (fileEntry.path) {
|
2025-12-06 10:36:17 -08:00
|
|
|
|
// Paths are relative to bmadDir. Legacy manifests incorrectly prefixed 'bmad/' -
|
|
|
|
|
|
// strip it if present. This is safe because no real path inside bmadDir would
|
2025-12-13 16:22:34 +08:00
|
|
|
|
// start with 'bmad/' (you'd never have _bmad/bmad/... as an actual structure).
|
2025-09-30 21:20:13 -05:00
|
|
|
|
const relativePath = fileEntry.path.startsWith('bmad/') ? fileEntry.path.slice(5) : fileEntry.path;
|
|
|
|
|
|
const absolutePath = path.join(bmadDir, relativePath);
|
|
|
|
|
|
installedFilesMap.set(path.normalize(absolutePath), {
|
|
|
|
|
|
hash: fileEntry.hash,
|
|
|
|
|
|
relativePath: relativePath,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-09-30 20:06:02 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Recursively scan bmadDir for all files
|
|
|
|
|
|
const scanDirectory = async (dir) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
|
const fullPath = path.join(dir, entry.name);
|
|
|
|
|
|
|
|
|
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
|
// Skip certain directories
|
|
|
|
|
|
if (entry.name === 'node_modules' || entry.name === '.git') {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
await scanDirectory(fullPath);
|
|
|
|
|
|
} else if (entry.isFile()) {
|
|
|
|
|
|
const normalizedPath = path.normalize(fullPath);
|
2025-09-30 21:20:13 -05:00
|
|
|
|
const fileInfo = installedFilesMap.get(normalizedPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Skip certain system files that are auto-generated
|
|
|
|
|
|
const relativePath = path.relative(bmadDir, fullPath);
|
|
|
|
|
|
const fileName = path.basename(fullPath);
|
|
|
|
|
|
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// Skip _config directory EXCEPT for modified agent customizations
|
|
|
|
|
|
if (relativePath.startsWith('_config/') || relativePath.startsWith('_config\\')) {
|
2025-12-13 19:23:02 +08:00
|
|
|
|
// Special handling for .customize.yaml files - only preserve if modified
|
|
|
|
|
|
if (relativePath.includes('/agents/') && fileName.endsWith('.customize.yaml')) {
|
|
|
|
|
|
// Check if the customization file has been modified from manifest
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
|
2025-12-13 19:23:02 +08:00
|
|
|
|
if (await fs.pathExists(manifestPath)) {
|
|
|
|
|
|
const crypto = require('node:crypto');
|
|
|
|
|
|
const currentContent = await fs.readFile(fullPath, 'utf8');
|
|
|
|
|
|
const currentHash = crypto.createHash('sha256').update(currentContent).digest('hex');
|
|
|
|
|
|
|
|
|
|
|
|
const yaml = require('yaml');
|
|
|
|
|
|
const manifestContent = await fs.readFile(manifestPath, 'utf8');
|
|
|
|
|
|
const manifestData = yaml.parse(manifestContent);
|
|
|
|
|
|
const originalHash = manifestData.agentCustomizations?.[relativePath];
|
|
|
|
|
|
|
|
|
|
|
|
// Only add to customFiles if hash differs (user modified)
|
|
|
|
|
|
if (originalHash && currentHash !== originalHash) {
|
|
|
|
|
|
customFiles.push(fullPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-30 21:20:13 -05:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Skip config.yaml files - these are regenerated on each install/update
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// Users should use _config/agents/ override files instead
|
2025-09-30 21:20:13 -05:00
|
|
|
|
if (fileName === 'config.yaml') {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!fileInfo) {
|
|
|
|
|
|
// File not in manifest = custom file
|
2025-12-13 19:23:02 +08:00
|
|
|
|
// EXCEPT: Agent .md files in module folders are generated files, not custom
|
2025-12-13 19:41:09 +08:00
|
|
|
|
// Only treat .md files under _config/agents/ as custom
|
|
|
|
|
|
if (!(fileName.endsWith('.md') && relativePath.includes('/agents/') && !relativePath.startsWith('_config/'))) {
|
2025-12-13 19:23:02 +08:00
|
|
|
|
customFiles.push(fullPath);
|
|
|
|
|
|
}
|
2025-09-30 21:20:13 -05:00
|
|
|
|
} else if (manifestHasHashes && fileInfo.hash) {
|
|
|
|
|
|
// File in manifest with hash - check if it was modified
|
|
|
|
|
|
const currentHash = await this.manifest.calculateFileHash(fullPath);
|
|
|
|
|
|
if (currentHash && currentHash !== fileInfo.hash) {
|
|
|
|
|
|
// Hash changed = file was modified
|
|
|
|
|
|
modifiedFiles.push({
|
|
|
|
|
|
path: fullPath,
|
|
|
|
|
|
relativePath: fileInfo.relativePath,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-09-30 20:06:02 -05:00
|
|
|
|
}
|
2025-09-30 21:20:13 -05:00
|
|
|
|
// If manifest doesn't have hashes, we can't detect modifications
|
|
|
|
|
|
// so we just skip files that are in the manifest
|
2025-09-30 20:06:02 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Ignore errors scanning directories
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await scanDirectory(bmadDir);
|
2025-09-30 21:20:13 -05:00
|
|
|
|
return { customFiles, modifiedFiles };
|
2025-09-30 20:06:02 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Private: Create agent configuration files
|
|
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
|
|
|
|
|
* @param {Object} userInfo - User information including name and language
|
|
|
|
|
|
*/
|
|
|
|
|
|
async createAgentConfigs(bmadDir, userInfo = null) {
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const agentConfigDir = path.join(bmadDir, '_config', 'agents');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
await fs.ensureDir(agentConfigDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Get all agents from all modules
|
|
|
|
|
|
const agents = [];
|
|
|
|
|
|
const agentDetails = []; // For manifest generation
|
|
|
|
|
|
|
|
|
|
|
|
// Check modules for agents (including core)
|
|
|
|
|
|
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
|
|
|
|
for (const entry of entries) {
|
2025-12-13 19:41:09 +08:00
|
|
|
|
if (entry.isDirectory() && entry.name !== '_config') {
|
2025-09-28 23:17:07 -05:00
|
|
|
|
const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents');
|
|
|
|
|
|
if (await fs.pathExists(moduleAgentsPath)) {
|
|
|
|
|
|
const agentFiles = await fs.readdir(moduleAgentsPath);
|
|
|
|
|
|
for (const agentFile of agentFiles) {
|
|
|
|
|
|
if (agentFile.endsWith('.md')) {
|
|
|
|
|
|
const agentPath = path.join(moduleAgentsPath, agentFile);
|
|
|
|
|
|
const agentContent = await fs.readFile(agentPath, 'utf8');
|
|
|
|
|
|
|
|
|
|
|
|
// Skip agents with localskip="true"
|
|
|
|
|
|
const hasLocalSkip = agentContent.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
|
|
|
|
|
|
if (hasLocalSkip) {
|
|
|
|
|
|
continue; // Skip this agent - it should not have been installed
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const agentName = path.basename(agentFile, '.md');
|
|
|
|
|
|
|
|
|
|
|
|
// Extract any nodes with agentConfig="true"
|
|
|
|
|
|
const agentConfigNodes = this.extractAgentConfigNodes(agentContent);
|
|
|
|
|
|
|
|
|
|
|
|
agents.push({
|
|
|
|
|
|
name: agentName,
|
|
|
|
|
|
module: entry.name,
|
|
|
|
|
|
agentConfigNodes: agentConfigNodes,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Use shared AgentPartyGenerator to extract details
|
|
|
|
|
|
let details = AgentPartyGenerator.extractAgentDetails(agentContent, entry.name, agentName);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply config overrides if they exist
|
|
|
|
|
|
if (details) {
|
|
|
|
|
|
const configPath = path.join(agentConfigDir, `${entry.name}-${agentName}.md`);
|
|
|
|
|
|
if (await fs.pathExists(configPath)) {
|
|
|
|
|
|
const configContent = await fs.readFile(configPath, 'utf8');
|
|
|
|
|
|
details = AgentPartyGenerator.applyConfigOverrides(details, configContent);
|
|
|
|
|
|
}
|
|
|
|
|
|
agentDetails.push(details);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create config file for each agent
|
|
|
|
|
|
let createdCount = 0;
|
|
|
|
|
|
let skippedCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Load agent config template
|
|
|
|
|
|
const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md');
|
|
|
|
|
|
const templateContent = await fs.readFile(templatePath, 'utf8');
|
|
|
|
|
|
|
|
|
|
|
|
for (const agent of agents) {
|
|
|
|
|
|
const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`);
|
|
|
|
|
|
|
|
|
|
|
|
// Skip if config file already exists (preserve custom configurations)
|
|
|
|
|
|
if (await fs.pathExists(configPath)) {
|
|
|
|
|
|
skippedCount++;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build config content header
|
|
|
|
|
|
let configContent = `# Agent Config: ${agent.name}\n\n`;
|
|
|
|
|
|
|
|
|
|
|
|
// Process template and add agent-specific config nodes
|
|
|
|
|
|
let processedTemplate = templateContent;
|
|
|
|
|
|
|
|
|
|
|
|
// Replace {core:user_name} placeholder with actual user name if available
|
|
|
|
|
|
if (userInfo && userInfo.userName) {
|
|
|
|
|
|
processedTemplate = processedTemplate.replaceAll('{core:user_name}', userInfo.userName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Replace {core:communication_language} placeholder with actual language if available
|
|
|
|
|
|
if (userInfo && userInfo.responseLanguage) {
|
|
|
|
|
|
processedTemplate = processedTemplate.replaceAll('{core:communication_language}', userInfo.responseLanguage);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If this agent has agentConfig nodes, add them after the existing comment
|
|
|
|
|
|
if (agent.agentConfigNodes && agent.agentConfigNodes.length > 0) {
|
|
|
|
|
|
// Find the agent-specific configuration nodes comment
|
|
|
|
|
|
const commentPattern = /(\s*<!-- Agent-specific configuration nodes -->)/;
|
|
|
|
|
|
const commentMatch = processedTemplate.match(commentPattern);
|
|
|
|
|
|
|
|
|
|
|
|
if (commentMatch) {
|
|
|
|
|
|
// Add nodes right after the comment
|
|
|
|
|
|
let agentSpecificNodes = '';
|
|
|
|
|
|
for (const node of agent.agentConfigNodes) {
|
|
|
|
|
|
agentSpecificNodes += `\n ${node}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
processedTemplate = processedTemplate.replace(commentPattern, `$1${agentSpecificNodes}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
configContent += processedTemplate;
|
|
|
|
|
|
|
2025-11-05 04:18:12 +02:00
|
|
|
|
// Ensure POSIX-compliant final newline
|
|
|
|
|
|
if (!configContent.endsWith('\n')) {
|
|
|
|
|
|
configContent += '\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
await fs.writeFile(configPath, configContent, 'utf8');
|
2025-09-30 21:20:13 -05:00
|
|
|
|
this.installedFiles.push(configPath); // Track agent config files
|
2025-09-28 23:17:07 -05:00
|
|
|
|
createdCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Generate agent manifest with overrides applied
|
|
|
|
|
|
await this.generateAgentManifest(bmadDir, agentDetails);
|
|
|
|
|
|
|
|
|
|
|
|
return { total: agents.length, created: createdCount, skipped: skippedCount };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Generate agent manifest XML file
|
|
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
|
|
|
|
|
* @param {Array} agentDetails - Array of agent details
|
|
|
|
|
|
*/
|
|
|
|
|
|
async generateAgentManifest(bmadDir, agentDetails) {
|
2025-12-13 19:41:09 +08:00
|
|
|
|
const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
await AgentPartyGenerator.writeAgentParty(manifestPath, agentDetails, { forWeb: false });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Extract nodes with agentConfig="true" from agent content
|
|
|
|
|
|
* @param {string} content - Agent file content
|
|
|
|
|
|
* @returns {Array} Array of XML nodes that should be added to agent config
|
|
|
|
|
|
*/
|
|
|
|
|
|
extractAgentConfigNodes(content) {
|
|
|
|
|
|
const nodes = [];
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Find all XML nodes with agentConfig="true"
|
|
|
|
|
|
// Match self-closing tags and tags with content
|
|
|
|
|
|
const selfClosingPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*\/>/g;
|
|
|
|
|
|
const withContentPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*>([\s\S]*?)<\/\1>/g;
|
|
|
|
|
|
|
|
|
|
|
|
// Extract self-closing tags
|
|
|
|
|
|
let match;
|
|
|
|
|
|
while ((match = selfClosingPattern.exec(content)) !== null) {
|
|
|
|
|
|
// Extract just the tag without children (structure only)
|
|
|
|
|
|
const tagMatch = match[0].match(/<([a-zA-Z][a-zA-Z0-9_-]*)([^>]*)\/>/);
|
|
|
|
|
|
if (tagMatch) {
|
|
|
|
|
|
const tagName = tagMatch[1];
|
|
|
|
|
|
const attributes = tagMatch[2].replace(/\s*agentConfig="true"/, ''); // Remove agentConfig attribute
|
|
|
|
|
|
nodes.push(`<${tagName}${attributes}></${tagName}>`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Extract tags with content
|
|
|
|
|
|
while ((match = withContentPattern.exec(content)) !== null) {
|
|
|
|
|
|
const fullMatch = match[0];
|
|
|
|
|
|
const tagName = match[1];
|
|
|
|
|
|
|
|
|
|
|
|
// Extract opening tag with attributes (removing agentConfig="true")
|
|
|
|
|
|
const openingTagMatch = fullMatch.match(new RegExp(`<${tagName}([^>]*)>`));
|
|
|
|
|
|
if (openingTagMatch) {
|
|
|
|
|
|
const attributes = openingTagMatch[1].replace(/\s*agentConfig="true"/, '');
|
|
|
|
|
|
// Add empty node structure (no children)
|
|
|
|
|
|
nodes.push(`<${tagName}${attributes}></${tagName}>`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error extracting agentConfig nodes:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nodes;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Copy IDE-specific documentation to BMAD docs
|
|
|
|
|
|
* @param {Array} ides - List of selected IDEs
|
|
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
|
|
|
|
|
*/
|
|
|
|
|
|
async copyIdeDocumentation(ides, bmadDir) {
|
|
|
|
|
|
const docsDir = path.join(bmadDir, 'docs');
|
|
|
|
|
|
await fs.ensureDir(docsDir);
|
|
|
|
|
|
|
|
|
|
|
|
for (const ide of ides) {
|
|
|
|
|
|
const sourceDocPath = path.join(getProjectRoot(), 'docs', 'ide-info', `${ide}.md`);
|
|
|
|
|
|
const targetDocPath = path.join(docsDir, `${ide}-instructions.md`);
|
|
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(sourceDocPath)) {
|
2025-11-08 13:58:43 -06:00
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourceDocPath, targetDocPath, this.bmadFolderName || 'bmad');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-07 20:46:09 -06:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handle missing custom module sources interactively
|
|
|
|
|
|
* @param {Map} customModuleSources - Map of custom module ID to info
|
|
|
|
|
|
* @param {string} bmadDir - BMAD directory
|
|
|
|
|
|
* @param {string} projectRoot - Project root directory
|
|
|
|
|
|
* @param {string} operation - Current operation ('update', 'compile', etc.)
|
|
|
|
|
|
* @param {Array} installedModules - Array of installed module IDs (will be modified)
|
|
|
|
|
|
* @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
|
|
|
|
|
|
*/
|
|
|
|
|
|
async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules) {
|
|
|
|
|
|
const validCustomModules = [];
|
|
|
|
|
|
const keptModulesWithoutSources = []; // Track modules kept without sources
|
|
|
|
|
|
const customModulesWithMissingSources = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Check which sources exist
|
|
|
|
|
|
for (const [moduleId, customInfo] of customModuleSources) {
|
|
|
|
|
|
if (await fs.pathExists(customInfo.sourcePath)) {
|
|
|
|
|
|
validCustomModules.push({
|
|
|
|
|
|
id: moduleId,
|
|
|
|
|
|
name: customInfo.name,
|
|
|
|
|
|
path: customInfo.sourcePath,
|
|
|
|
|
|
info: customInfo,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
customModulesWithMissingSources.push({
|
|
|
|
|
|
id: moduleId,
|
|
|
|
|
|
name: customInfo.name,
|
|
|
|
|
|
sourcePath: customInfo.sourcePath,
|
|
|
|
|
|
relativePath: customInfo.relativePath,
|
|
|
|
|
|
info: customInfo,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If no missing sources, return immediately
|
|
|
|
|
|
if (customModulesWithMissingSources.length === 0) {
|
2025-12-08 12:24:30 -07:00
|
|
|
|
return {
|
|
|
|
|
|
validCustomModules,
|
|
|
|
|
|
keptModulesWithoutSources: [],
|
|
|
|
|
|
};
|
2025-12-07 20:46:09 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Stop any spinner for interactive prompts
|
|
|
|
|
|
const currentSpinner = ora();
|
|
|
|
|
|
if (currentSpinner.isSpinning) {
|
|
|
|
|
|
currentSpinner.stop();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
|
|
|
|
|
|
|
|
|
|
|
|
const inquirer = require('inquirer');
|
|
|
|
|
|
let keptCount = 0;
|
|
|
|
|
|
let updatedCount = 0;
|
|
|
|
|
|
let removedCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (const missing of customModulesWithMissingSources) {
|
|
|
|
|
|
console.log(chalk.dim(` • ${missing.name} (${missing.id})`));
|
|
|
|
|
|
console.log(chalk.dim(` Original source: ${missing.relativePath}`));
|
|
|
|
|
|
console.log(chalk.dim(` Full path: ${missing.sourcePath}`));
|
|
|
|
|
|
|
|
|
|
|
|
const choices = [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'Keep installed (will not be processed)',
|
|
|
|
|
|
value: 'keep',
|
|
|
|
|
|
short: 'Keep',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'Specify new source location',
|
|
|
|
|
|
value: 'update',
|
|
|
|
|
|
short: 'Update',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// Only add remove option if not just compiling agents
|
|
|
|
|
|
if (operation !== 'compile-agents') {
|
|
|
|
|
|
choices.push({
|
|
|
|
|
|
name: '⚠️ REMOVE module completely (destructive!)',
|
|
|
|
|
|
value: 'remove',
|
|
|
|
|
|
short: 'Remove',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { action } = await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'list',
|
|
|
|
|
|
name: 'action',
|
|
|
|
|
|
message: `How would you like to handle "${missing.name}"?`,
|
|
|
|
|
|
choices,
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
|
case 'update': {
|
|
|
|
|
|
const { newSourcePath } = await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'input',
|
|
|
|
|
|
name: 'newSourcePath',
|
|
|
|
|
|
message: 'Enter the new path to the custom module:',
|
|
|
|
|
|
default: missing.sourcePath,
|
|
|
|
|
|
validate: async (input) => {
|
|
|
|
|
|
if (!input || input.trim() === '') {
|
|
|
|
|
|
return 'Please enter a path';
|
|
|
|
|
|
}
|
|
|
|
|
|
const expandedPath = path.resolve(input.trim());
|
|
|
|
|
|
if (!(await fs.pathExists(expandedPath))) {
|
|
|
|
|
|
return 'Path does not exist';
|
|
|
|
|
|
}
|
|
|
|
|
|
// Check if it looks like a valid module
|
|
|
|
|
|
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
|
|
|
|
const agentsPath = path.join(expandedPath, 'agents');
|
|
|
|
|
|
const workflowsPath = path.join(expandedPath, 'workflows');
|
|
|
|
|
|
|
|
|
|
|
|
if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) {
|
|
|
|
|
|
return 'Path does not appear to contain a valid custom module';
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// Update the source in manifest
|
|
|
|
|
|
const resolvedPath = path.resolve(newSourcePath.trim());
|
|
|
|
|
|
missing.info.sourcePath = resolvedPath;
|
|
|
|
|
|
// Remove relativePath - we only store absolute sourcePath now
|
|
|
|
|
|
delete missing.info.relativePath;
|
|
|
|
|
|
await this.manifest.addCustomModule(bmadDir, missing.info);
|
|
|
|
|
|
|
|
|
|
|
|
validCustomModules.push({
|
|
|
|
|
|
id: moduleId,
|
|
|
|
|
|
name: missing.name,
|
|
|
|
|
|
path: resolvedPath,
|
|
|
|
|
|
info: missing.info,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
updatedCount++;
|
|
|
|
|
|
console.log(chalk.green(`✓ Updated source location`));
|
|
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'remove': {
|
|
|
|
|
|
// Extra confirmation for destructive remove
|
|
|
|
|
|
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
|
|
|
|
|
|
console.log(chalk.red(` Module location: ${path.join(bmadDir, moduleId)}`));
|
|
|
|
|
|
|
|
|
|
|
|
const { confirm } = await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'confirm',
|
|
|
|
|
|
name: 'confirm',
|
|
|
|
|
|
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
|
|
|
|
|
|
default: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
if (confirm) {
|
|
|
|
|
|
const { typedConfirm } = await inquirer.prompt([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'input',
|
|
|
|
|
|
name: 'typedConfirm',
|
|
|
|
|
|
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
|
|
|
|
|
|
validate: (input) => {
|
|
|
|
|
|
if (input !== 'DELETE') {
|
|
|
|
|
|
return chalk.red('You must type "DELETE" exactly to proceed');
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
if (typedConfirm === 'DELETE') {
|
|
|
|
|
|
// Remove the module from filesystem and manifest
|
|
|
|
|
|
const modulePath = path.join(bmadDir, moduleId);
|
|
|
|
|
|
if (await fs.pathExists(modulePath)) {
|
|
|
|
|
|
const fsExtra = require('fs-extra');
|
|
|
|
|
|
await fsExtra.remove(modulePath);
|
|
|
|
|
|
console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.manifest.removeModule(bmadDir, moduleId);
|
|
|
|
|
|
await this.manifest.removeCustomModule(bmadDir, moduleId);
|
|
|
|
|
|
console.log(chalk.yellow(` ✓ Removed from manifest`));
|
|
|
|
|
|
|
|
|
|
|
|
// Also remove from installedModules list
|
|
|
|
|
|
if (installedModules && installedModules.includes(moduleId)) {
|
|
|
|
|
|
const index = installedModules.indexOf(moduleId);
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
installedModules.splice(index, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
removedCount++;
|
|
|
|
|
|
console.log(chalk.red.bold(`✓ "${missing.name}" has been permanently removed`));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.dim(' Removal cancelled - module will be kept'));
|
|
|
|
|
|
keptCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.dim(' Removal cancelled - module will be kept'));
|
|
|
|
|
|
keptCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'keep': {
|
|
|
|
|
|
keptCount++;
|
|
|
|
|
|
keptModulesWithoutSources.push(moduleId);
|
|
|
|
|
|
console.log(chalk.dim(` Module will be kept as-is`));
|
|
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
// No default
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show summary
|
|
|
|
|
|
if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
|
|
|
|
|
|
console.log(chalk.dim(`\nSummary for custom modules with missing sources:`));
|
|
|
|
|
|
if (keptCount > 0) console.log(chalk.dim(` • ${keptCount} module(s) kept as-is`));
|
|
|
|
|
|
if (updatedCount > 0) console.log(chalk.dim(` • ${updatedCount} module(s) updated with new sources`));
|
|
|
|
|
|
if (removedCount > 0) console.log(chalk.red(` • ${removedCount} module(s) permanently deleted`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
validCustomModules,
|
|
|
|
|
|
keptModulesWithoutSources,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = { Installer };
|