2025-09-28 23:17:07 -05:00
|
|
|
|
const chalk = require('chalk');
|
|
|
|
|
|
const path = require('node:path');
|
2025-12-05 22:32:59 -06:00
|
|
|
|
const fs = require('fs-extra');
|
2025-09-28 23:17:07 -05:00
|
|
|
|
const { Installer } = require('../installers/lib/core/installer');
|
|
|
|
|
|
const { UI } = require('../lib/ui');
|
|
|
|
|
|
|
|
|
|
|
|
const installer = new Installer();
|
|
|
|
|
|
const ui = new UI();
|
|
|
|
|
|
|
2025-12-05 22:32:59 -06:00
|
|
|
|
/**
|
|
|
|
|
|
* Install custom content (agents, workflows, modules)
|
|
|
|
|
|
* @param {Object} config - Installation configuration
|
|
|
|
|
|
* @param {Object} result - Installation result
|
|
|
|
|
|
* @param {string} projectDirectory - Project directory path
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function installCustomContent(config, result, projectDirectory) {
|
|
|
|
|
|
const { customContent } = config;
|
|
|
|
|
|
const { selectedItems } = customContent;
|
|
|
|
|
|
const projectDir = projectDirectory;
|
|
|
|
|
|
const bmadDir = result.path;
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.dim(`Project: ${projectDir}`));
|
|
|
|
|
|
console.log(chalk.dim(`BMAD: ${bmadDir}`));
|
|
|
|
|
|
|
|
|
|
|
|
// Install custom agents - use agent-install logic
|
|
|
|
|
|
if (selectedItems.agents.length > 0) {
|
|
|
|
|
|
console.log(chalk.blue(`\n👥 Installing ${selectedItems.agents.length} custom agent(s)...`));
|
|
|
|
|
|
for (const agent of selectedItems.agents) {
|
|
|
|
|
|
await installCustomAgentWithPrompts(agent, projectDir, bmadDir, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Install custom workflows - copy and register with IDEs
|
|
|
|
|
|
if (selectedItems.workflows.length > 0) {
|
|
|
|
|
|
console.log(chalk.blue(`\n📋 Installing ${selectedItems.workflows.length} custom workflow(s)...`));
|
|
|
|
|
|
for (const workflow of selectedItems.workflows) {
|
|
|
|
|
|
await installCustomWorkflowWithIDE(workflow, projectDir, bmadDir, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Install custom modules - treat like regular modules
|
|
|
|
|
|
if (selectedItems.modules.length > 0) {
|
|
|
|
|
|
console.log(chalk.blue(`\n🔧 Installing ${selectedItems.modules.length} custom module(s)...`));
|
|
|
|
|
|
for (const module of selectedItems.modules) {
|
|
|
|
|
|
await installCustomModuleAsRegular(module, projectDir, bmadDir, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.green('\n✓ Custom content installation complete!'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Install a custom agent with proper prompts (mirrors agent-install.js)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function installCustomAgentWithPrompts(agent, projectDir, bmadDir, config) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
discoverAgents,
|
|
|
|
|
|
loadAgentConfig,
|
|
|
|
|
|
addToManifest,
|
|
|
|
|
|
extractManifestData,
|
|
|
|
|
|
promptInstallQuestions,
|
|
|
|
|
|
createIdeSlashCommands,
|
|
|
|
|
|
updateManifestYaml,
|
|
|
|
|
|
saveAgentSource,
|
|
|
|
|
|
} = require('../lib/agent/installer');
|
|
|
|
|
|
const { compileAgent } = require('../lib/agent/compiler');
|
|
|
|
|
|
const inquirer = require('inquirer');
|
|
|
|
|
|
const readline = require('node:readline');
|
|
|
|
|
|
const yaml = require('js-yaml');
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.cyan(` Installing agent: ${agent.name}`));
|
|
|
|
|
|
|
|
|
|
|
|
// Load agent config
|
|
|
|
|
|
const agentConfig = loadAgentConfig(agent.yamlPath);
|
|
|
|
|
|
const agentType = agent.name; // e.g., "toolsmith"
|
|
|
|
|
|
|
|
|
|
|
|
// Confirm/customize agent persona name (mirrors agent-install.js)
|
|
|
|
|
|
const rl1 = readline.createInterface({
|
|
|
|
|
|
input: process.stdin,
|
|
|
|
|
|
output: process.stdout,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const defaultPersonaName = agentConfig.metadata.name || agentType;
|
|
|
|
|
|
console.log(chalk.cyan(`\n 📛 Agent Persona Name`));
|
|
|
|
|
|
console.log(chalk.dim(` Agent type: ${agentType}`));
|
|
|
|
|
|
console.log(chalk.dim(` Default persona: ${defaultPersonaName}`));
|
|
|
|
|
|
console.log(chalk.dim(' Leave blank to use default, or provide a custom name.'));
|
|
|
|
|
|
console.log(chalk.dim(' Examples:'));
|
|
|
|
|
|
console.log(chalk.dim(` - (blank) → "${defaultPersonaName}" as ${agentType}.md`));
|
|
|
|
|
|
console.log(chalk.dim(` - "Fred" → "Fred" as fred-${agentType}.md`));
|
|
|
|
|
|
console.log(chalk.dim(` - "Captain Code" → "Captain Code" as captain-code-${agentType}.md`));
|
|
|
|
|
|
|
|
|
|
|
|
const customPersonaName = await new Promise((resolve) => {
|
|
|
|
|
|
rl1.question(`\n Custom name (or Enter for default): `, resolve);
|
|
|
|
|
|
});
|
|
|
|
|
|
rl1.close();
|
|
|
|
|
|
|
|
|
|
|
|
// Determine final agent file name based on persona name
|
|
|
|
|
|
let finalAgentName;
|
|
|
|
|
|
let personaName;
|
|
|
|
|
|
if (customPersonaName.trim()) {
|
|
|
|
|
|
personaName = customPersonaName.trim();
|
|
|
|
|
|
const namePrefix = personaName.toLowerCase().replaceAll(/\s+/g, '-');
|
|
|
|
|
|
finalAgentName = `${namePrefix}-${agentType}`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
personaName = defaultPersonaName;
|
|
|
|
|
|
finalAgentName = agentType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.dim(` Persona: ${personaName}`));
|
|
|
|
|
|
console.log(chalk.dim(` File: ${finalAgentName}.md`));
|
|
|
|
|
|
|
|
|
|
|
|
// Get answers (prompt or use defaults)
|
|
|
|
|
|
let presetAnswers = {};
|
|
|
|
|
|
|
|
|
|
|
|
// If custom persona name provided, inject it as custom_name for template processing
|
|
|
|
|
|
if (customPersonaName.trim()) {
|
|
|
|
|
|
presetAnswers.custom_name = personaName;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let answers;
|
|
|
|
|
|
if (agentConfig.installConfig) {
|
|
|
|
|
|
answers = await promptInstallQuestions(agentConfig.installConfig, agentConfig.defaults, presetAnswers);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
answers = { ...agentConfig.defaults, ...presetAnswers };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create target directory
|
|
|
|
|
|
const targetDir = path.join(bmadDir, 'custom', 'agents', finalAgentName);
|
|
|
|
|
|
await fs.ensureDir(targetDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Compile agent with answers
|
|
|
|
|
|
const { xml, metadata } = compileAgent(
|
|
|
|
|
|
agentConfig.yamlContent,
|
|
|
|
|
|
answers,
|
|
|
|
|
|
finalAgentName,
|
|
|
|
|
|
`.bmad/custom/agents/${finalAgentName}/${finalAgentName}.md`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Write compiled agent
|
|
|
|
|
|
const compiledPath = path.join(targetDir, `${finalAgentName}.md`);
|
|
|
|
|
|
await fs.writeFile(compiledPath, xml, 'utf8');
|
|
|
|
|
|
|
|
|
|
|
|
// Copy sidecar files if exists
|
|
|
|
|
|
if (agent.hasSidecar) {
|
|
|
|
|
|
const entries = await fs.readdir(agent.path, { withFileTypes: true });
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
|
if (entry.isFile() && !entry.name.endsWith('.agent.yaml')) {
|
|
|
|
|
|
await fs.copy(path.join(agent.path, entry.name), path.join(targetDir, entry.name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Save source YAML for reinstallation
|
|
|
|
|
|
const cfgAgentsBackupDir = path.join(bmadDir, '_cfg', 'custom', 'agents');
|
|
|
|
|
|
await fs.ensureDir(cfgAgentsBackupDir);
|
|
|
|
|
|
const backupYamlPath = path.join(cfgAgentsBackupDir, `${finalAgentName}.agent.yaml`);
|
|
|
|
|
|
await fs.copy(agent.yamlPath, backupYamlPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Add to agent manifest
|
|
|
|
|
|
const manifestFile = path.join(bmadDir, '_cfg', 'agent-manifest.csv');
|
|
|
|
|
|
const relativePath = `.bmad/custom/agents/${finalAgentName}/${finalAgentName}.md`;
|
|
|
|
|
|
const manifestData = extractManifestData(xml, { ...metadata, name: finalAgentName }, relativePath, 'custom');
|
|
|
|
|
|
manifestData.name = finalAgentName;
|
|
|
|
|
|
manifestData.displayName = metadata.name || finalAgentName;
|
|
|
|
|
|
addToManifest(manifestFile, manifestData);
|
|
|
|
|
|
|
|
|
|
|
|
// Update manifest.yaml
|
|
|
|
|
|
const manifestYamlPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
|
|
|
|
|
|
updateManifestYaml(manifestYamlPath, finalAgentName, finalAgentName);
|
|
|
|
|
|
|
|
|
|
|
|
// Create IDE slash commands using existing IDEs from config
|
|
|
|
|
|
const ideResults = await createIdeSlashCommands(projectDir, finalAgentName, relativePath, metadata, config.ides || []);
|
|
|
|
|
|
const ideCount = Object.keys(ideResults).length;
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.green(` ✓ ${finalAgentName} (registered with ${ideCount} IDE${ideCount === 1 ? '' : 's'})`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Install a custom workflow and register with all IDEs
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function installCustomWorkflowWithIDE(workflow, projectDir, bmadDir, config) {
|
|
|
|
|
|
const targetDir = path.join(bmadDir, 'custom', 'workflows');
|
|
|
|
|
|
|
|
|
|
|
|
// Check if workflow is a directory or just a file
|
|
|
|
|
|
// workflow.path might be a file (workflow.md) or a directory
|
|
|
|
|
|
let sourcePath = workflow.path;
|
|
|
|
|
|
let isDirectory = false;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const stats = await fs.stat(workflow.path);
|
|
|
|
|
|
isDirectory = stats.isDirectory();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.log(chalk.red(` ERROR: Cannot access workflow path: ${workflow.path}`));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If it's a file ending in workflow.md, use the parent directory
|
|
|
|
|
|
if (!isDirectory && workflow.path.endsWith('workflow.md')) {
|
|
|
|
|
|
sourcePath = path.dirname(workflow.path);
|
|
|
|
|
|
isDirectory = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isDirectory) {
|
|
|
|
|
|
// Copy the entire workflow directory
|
|
|
|
|
|
const workflowName = path.basename(sourcePath);
|
|
|
|
|
|
const targetWorkflowDir = path.join(targetDir, workflowName);
|
|
|
|
|
|
await fs.copy(sourcePath, targetWorkflowDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Update manifest with the main workflow.md file
|
|
|
|
|
|
const relativePath = `.bmad/custom/workflows/${workflowName}/workflow.md`;
|
|
|
|
|
|
await addWorkflowToManifest(bmadDir, workflow.name, workflow.description, relativePath, 'custom');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Single file workflow
|
|
|
|
|
|
const targetFileName = path.basename(sourcePath);
|
|
|
|
|
|
const targetPath = path.join(targetDir, targetFileName);
|
|
|
|
|
|
await fs.copy(sourcePath, targetPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Update manifest
|
|
|
|
|
|
const relativePath = `.bmad/custom/workflows/${targetFileName}`;
|
|
|
|
|
|
await addWorkflowToManifest(bmadDir, workflow.name, workflow.description, relativePath, 'custom');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Register workflow with all configured IDEs
|
|
|
|
|
|
const relativePath = `.bmad/custom/workflows/${path.basename(workflow.path)}`;
|
|
|
|
|
|
if (config.ides && config.ides.length > 0) {
|
|
|
|
|
|
const { IdeManager } = require('../installers/lib/ide/manager');
|
|
|
|
|
|
const ideManager = new IdeManager();
|
|
|
|
|
|
|
|
|
|
|
|
for (const ide of config.ides) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// IdeManager uses a Map, not getHandler method
|
|
|
|
|
|
const ideHandler = ideManager.handlers.get(ide.toLowerCase());
|
|
|
|
|
|
if (ideHandler && typeof ideHandler.registerWorkflow === 'function') {
|
|
|
|
|
|
await ideHandler.registerWorkflow(projectDir, bmadDir, workflow.name, relativePath);
|
|
|
|
|
|
console.log(chalk.dim(` ✓ Registered with ${ide}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.log(chalk.yellow(` ⚠️ Could not register with ${ide}: ${error.message}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.green(` ✓ ${workflow.name} (copied to custom workflows and registered with IDEs)`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Helper to add workflow to manifest
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function addWorkflowToManifest(bmadDir, name, description, relativePath, moduleType = 'custom') {
|
|
|
|
|
|
const workflowManifestPath = path.join(bmadDir, '_cfg', 'workflow-manifest.csv');
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.dim(`[DEBUG] Adding workflow to manifest: ${name} -> ${relativePath} (module: ${moduleType})`));
|
|
|
|
|
|
|
|
|
|
|
|
// Read existing manifest
|
|
|
|
|
|
let manifestContent = '';
|
|
|
|
|
|
if (await fs.pathExists(workflowManifestPath)) {
|
|
|
|
|
|
manifestContent = await fs.readFile(workflowManifestPath, 'utf8');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure header exists
|
|
|
|
|
|
if (!manifestContent.includes('name,description,module,path')) {
|
|
|
|
|
|
manifestContent = 'name,description,module,path\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add workflow entry
|
|
|
|
|
|
const csvLine = `"${name}","${description}","${moduleType}","${relativePath}"\n`;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if workflow already exists in manifest
|
|
|
|
|
|
if (manifestContent.includes(`"${name}",`)) {
|
|
|
|
|
|
console.log(chalk.dim(`[DEBUG] Workflow already exists in manifest: ${name}`));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await fs.writeFile(workflowManifestPath, manifestContent + csvLine);
|
|
|
|
|
|
console.log(chalk.dim(`[DEBUG] Successfully added to manifest`));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.log(chalk.red(`[ERROR] Failed to write to manifest: ${error.message}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Install a custom module like a regular module
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function installCustomModuleAsRegular(module, projectDir, bmadDir, config) {
|
|
|
|
|
|
const yaml = require('js-yaml');
|
|
|
|
|
|
const path = require('node:path');
|
|
|
|
|
|
|
|
|
|
|
|
// The custom module path should be the source location
|
|
|
|
|
|
const customSrcPath = module.path;
|
|
|
|
|
|
|
|
|
|
|
|
// Install the custom module by copying it to the custom modules directory
|
|
|
|
|
|
const targetDir = path.join(bmadDir, 'custom', 'modules', module.name);
|
|
|
|
|
|
await fs.copy(customSrcPath, targetDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Check if module has an installer and run it from the ORIGINAL source location
|
|
|
|
|
|
const installerPath = path.join(customSrcPath, '_module-installer', 'installer.js');
|
|
|
|
|
|
if (await fs.pathExists(installerPath)) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Clear require cache to ensure fresh import
|
|
|
|
|
|
delete require.cache[require.resolve(installerPath)];
|
|
|
|
|
|
|
|
|
|
|
|
// Load and run the module installer
|
|
|
|
|
|
const moduleInstaller = require(installerPath);
|
|
|
|
|
|
await moduleInstaller.install({
|
|
|
|
|
|
projectRoot: projectDir,
|
|
|
|
|
|
config: config.coreConfig || {},
|
|
|
|
|
|
installedIDEs: config.ides || [],
|
|
|
|
|
|
logger: {
|
|
|
|
|
|
log: (msg) => console.log(chalk.dim(` ${msg}`)),
|
|
|
|
|
|
error: (msg) => console.log(chalk.red(` ERROR: ${msg}`)),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log(chalk.green(` ✓ ${module.name} (custom installer executed)`));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.log(chalk.yellow(` ⚠️ ${module.name} installer failed: ${error.message}`));
|
|
|
|
|
|
console.log(chalk.dim(` Module copied but not configured`));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// No installer - check if module has agents/workflows to install
|
|
|
|
|
|
console.log(chalk.dim(` Processing module agents and workflows...`));
|
|
|
|
|
|
|
|
|
|
|
|
// Install agents from the module
|
|
|
|
|
|
const agentsPath = path.join(customSrcPath, 'agents');
|
|
|
|
|
|
if (await fs.pathExists(agentsPath)) {
|
|
|
|
|
|
const agentFiles = await fs.readdir(agentsPath);
|
|
|
|
|
|
for (const agentFile of agentFiles) {
|
|
|
|
|
|
if (agentFile.endsWith('.yaml')) {
|
|
|
|
|
|
const agentPath = path.join(agentsPath, agentFile);
|
|
|
|
|
|
await installModuleAgent(agentPath, module.name, projectDir, bmadDir, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Install workflows from the module
|
|
|
|
|
|
const workflowsPath = path.join(customSrcPath, 'workflows');
|
|
|
|
|
|
if (await fs.pathExists(workflowsPath)) {
|
|
|
|
|
|
const workflowDirs = await fs.readdir(workflowsPath, { withFileTypes: true });
|
|
|
|
|
|
for (const workflowDir of workflowDirs) {
|
|
|
|
|
|
if (workflowDir.isDirectory()) {
|
|
|
|
|
|
const workflowPath = path.join(workflowsPath, workflowDir.name);
|
|
|
|
|
|
await installModuleWorkflow(workflowPath, module.name, projectDir, bmadDir, config);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.green(` ✓ ${module.name}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update manifest.yaml to include custom module with proper prefix
|
|
|
|
|
|
const manifestYamlPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
|
|
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(manifestYamlPath)) {
|
|
|
|
|
|
const manifest = yaml.load(await fs.readFile(manifestYamlPath, 'utf8'));
|
|
|
|
|
|
|
|
|
|
|
|
// Remove any old entries without custom- prefix for this module
|
|
|
|
|
|
const oldModuleName = module.name;
|
|
|
|
|
|
if (manifest.modules.includes(oldModuleName)) {
|
|
|
|
|
|
manifest.modules = manifest.modules.filter((m) => m !== oldModuleName);
|
|
|
|
|
|
console.log(chalk.dim(` Removed old entry: ${oldModuleName}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Custom modules should be stored with custom- prefix
|
|
|
|
|
|
const moduleNameWithPrefix = `custom-${module.name}`;
|
|
|
|
|
|
if (!manifest.modules.includes(moduleNameWithPrefix)) {
|
|
|
|
|
|
manifest.modules.push(moduleNameWithPrefix);
|
|
|
|
|
|
console.log(chalk.dim(` Added to manifest.yaml as ${moduleNameWithPrefix}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Write back the cleaned manifest
|
|
|
|
|
|
await fs.writeFile(manifestYamlPath, yaml.dump(manifest), 'utf8');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Register module with IDEs (like regular modules do)
|
|
|
|
|
|
if (config.ides && config.ides.length > 0) {
|
|
|
|
|
|
const { IdeManager } = require('../installers/lib/ide/manager');
|
|
|
|
|
|
const ideManager = new IdeManager();
|
|
|
|
|
|
|
|
|
|
|
|
for (const ide of config.ides) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// IdeManager uses a Map, not direct property access
|
|
|
|
|
|
const handler = ideManager.handlers.get(ide.toLowerCase());
|
|
|
|
|
|
if (handler && handler.moduleInjector) {
|
|
|
|
|
|
// Check if module has IDE-specific customizations
|
|
|
|
|
|
const subModulePath = path.join(customSrcPath, 'sub-modules', ide);
|
|
|
|
|
|
if (await fs.pathExists(subModulePath)) {
|
|
|
|
|
|
console.log(chalk.dim(` ✓ Found ${ide} customizations for ${module.name}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.log(chalk.yellow(` ⚠️ Could not configure ${ide} for ${module.name}: ${error.message}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Install an agent from a module
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function installModuleAgent(agentPath, moduleName, projectDir, bmadDir, config) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
loadAgentConfig,
|
|
|
|
|
|
addToManifest,
|
|
|
|
|
|
extractManifestData,
|
|
|
|
|
|
createIdeSlashCommands,
|
|
|
|
|
|
updateManifestYaml,
|
|
|
|
|
|
} = require('../lib/agent/installer');
|
|
|
|
|
|
const { compileAgent } = require('../lib/agent/compiler');
|
|
|
|
|
|
|
|
|
|
|
|
const agentName = path.basename(agentPath, '.yaml');
|
|
|
|
|
|
console.log(chalk.dim(` Installing agent: ${agentName} (from ${moduleName})`));
|
|
|
|
|
|
|
|
|
|
|
|
// Load agent config
|
|
|
|
|
|
const agentConfig = loadAgentConfig(agentPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Compile agent with defaults (no prompts for module agents)
|
|
|
|
|
|
const { xml, metadata } = compileAgent(
|
|
|
|
|
|
agentConfig.yamlContent,
|
|
|
|
|
|
agentConfig.defaults || {},
|
|
|
|
|
|
agentName,
|
|
|
|
|
|
`.bmad/custom/modules/${moduleName}/agents/${agentName}.md`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Create target directory
|
|
|
|
|
|
const targetDir = path.join(bmadDir, 'custom', 'modules', moduleName, 'agents');
|
|
|
|
|
|
await fs.ensureDir(targetDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Write compiled agent
|
|
|
|
|
|
const compiledPath = path.join(targetDir, `${agentName}.md`);
|
|
|
|
|
|
await fs.writeFile(compiledPath, xml, 'utf8');
|
|
|
|
|
|
|
|
|
|
|
|
// Remove the raw YAML file after compilation
|
|
|
|
|
|
const yamlPath = path.join(targetDir, `${agentName}.yaml`);
|
|
|
|
|
|
if (await fs.pathExists(yamlPath)) {
|
|
|
|
|
|
await fs.remove(yamlPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add to agent manifest
|
|
|
|
|
|
const manifestFile = path.join(bmadDir, '_cfg', 'agent-manifest.csv');
|
|
|
|
|
|
const relativePath = `.bmad/custom/modules/${moduleName}/agents/${agentName}.md`;
|
|
|
|
|
|
const manifestData = extractManifestData(xml, { ...metadata, name: agentName }, relativePath, 'custom');
|
|
|
|
|
|
manifestData.name = `${moduleName}-${agentName}`;
|
|
|
|
|
|
manifestData.displayName = metadata.name || agentName;
|
|
|
|
|
|
addToManifest(manifestFile, manifestData);
|
|
|
|
|
|
|
|
|
|
|
|
// Update manifest.yaml
|
|
|
|
|
|
const manifestYamlPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
|
|
|
|
|
|
updateManifestYaml(manifestYamlPath, `${moduleName}-${agentName}`, agentName);
|
|
|
|
|
|
|
|
|
|
|
|
// Create IDE slash commands
|
|
|
|
|
|
const ideResults = await createIdeSlashCommands(projectDir, `${moduleName}-${agentName}`, relativePath, metadata, config.ides || []);
|
|
|
|
|
|
const ideCount = Object.keys(ideResults).length;
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.dim(` ✓ ${agentName} (registered with ${ideCount} IDE${ideCount === 1 ? '' : 's'})`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Install a workflow from a module
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function installModuleWorkflow(workflowPath, moduleName, projectDir, bmadDir, config) {
|
|
|
|
|
|
const workflowName = path.basename(workflowPath);
|
|
|
|
|
|
|
|
|
|
|
|
// Copy the workflow directory
|
|
|
|
|
|
const targetDir = path.join(bmadDir, 'custom', 'modules', moduleName, 'workflows', workflowName);
|
|
|
|
|
|
await fs.copy(workflowPath, targetDir);
|
|
|
|
|
|
|
|
|
|
|
|
// Add to workflow manifest
|
|
|
|
|
|
const workflowManifestPath = path.join(bmadDir, '_cfg', 'workflow-manifest.csv');
|
|
|
|
|
|
const relativePath = `.bmad/custom/modules/${moduleName}/workflows/${workflowName}/README.md`;
|
|
|
|
|
|
|
|
|
|
|
|
// Read existing manifest
|
|
|
|
|
|
let manifestContent = '';
|
|
|
|
|
|
if (await fs.pathExists(workflowManifestPath)) {
|
|
|
|
|
|
manifestContent = await fs.readFile(workflowManifestPath, 'utf8');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure header exists
|
|
|
|
|
|
if (!manifestContent.includes('name,description,module,path')) {
|
|
|
|
|
|
manifestContent = 'name,description,module,path\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add workflow entry
|
|
|
|
|
|
const csvLine = `"${moduleName}-${workflowName}","Workflow from ${moduleName} module","${moduleName}","${relativePath}"\n`;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if workflow already exists in manifest
|
|
|
|
|
|
if (!manifestContent.includes(`"${moduleName}-${workflowName}",`)) {
|
|
|
|
|
|
await fs.writeFile(workflowManifestPath, manifestContent + csvLine);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Register with IDEs
|
|
|
|
|
|
if (config.ides && config.ides.length > 0) {
|
|
|
|
|
|
const { IdeManager } = require('../installers/lib/ide/manager');
|
|
|
|
|
|
const ideManager = new IdeManager();
|
|
|
|
|
|
|
|
|
|
|
|
for (const ide of config.ides) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const ideHandler = ideManager.handlers.get(ide.toLowerCase());
|
|
|
|
|
|
if (ideHandler && typeof ideHandler.registerWorkflow === 'function') {
|
|
|
|
|
|
await ideHandler.registerWorkflow(projectDir, bmadDir, `${moduleName}-${workflowName}`, relativePath);
|
|
|
|
|
|
console.log(chalk.dim(` ✓ Registered with ${ide}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.log(chalk.yellow(` ⚠️ Could not register with ${ide}: ${error.message}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.dim(` ✓ ${workflowName} workflow added and registered`));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-28 23:17:07 -05:00
|
|
|
|
module.exports = {
|
|
|
|
|
|
command: 'install',
|
|
|
|
|
|
description: 'Install BMAD Core agents and tools',
|
2025-11-26 16:47:15 -06:00
|
|
|
|
options: [['--skip-cleanup', 'Skip automatic cleanup of legacy files']],
|
|
|
|
|
|
action: async (options) => {
|
2025-09-28 23:17:07 -05:00
|
|
|
|
try {
|
|
|
|
|
|
const config = await ui.promptInstall();
|
2025-10-02 21:45:59 -05:00
|
|
|
|
|
2025-10-28 12:47:45 -05:00
|
|
|
|
// Handle cancel
|
|
|
|
|
|
if (config.actionType === 'cancel') {
|
|
|
|
|
|
console.log(chalk.yellow('Installation cancelled.'));
|
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-02 21:45:59 -05:00
|
|
|
|
// Handle agent compilation separately
|
|
|
|
|
|
if (config.actionType === 'compile') {
|
|
|
|
|
|
const result = await installer.compileAgents(config);
|
|
|
|
|
|
console.log(chalk.green('\n✨ Agent compilation complete!'));
|
|
|
|
|
|
console.log(chalk.cyan(`Rebuilt ${result.agentCount} agents and ${result.taskCount} tasks`));
|
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 16:17:37 -05:00
|
|
|
|
// Handle quick update separately
|
|
|
|
|
|
if (config.actionType === 'quick-update') {
|
|
|
|
|
|
const result = await installer.quickUpdate(config);
|
|
|
|
|
|
console.log(chalk.green('\n✨ Quick update complete!'));
|
|
|
|
|
|
console.log(chalk.cyan(`Updated ${result.moduleCount} modules with preserved settings`));
|
2025-12-05 22:32:59 -06:00
|
|
|
|
|
|
|
|
|
|
// After quick update, check for existing custom content and re-install to regenerate IDE commands
|
|
|
|
|
|
const { UI } = require('../lib/ui');
|
|
|
|
|
|
const ui = new UI();
|
|
|
|
|
|
const customPath = path.join(config.directory, 'bmad-custom-src');
|
|
|
|
|
|
|
|
|
|
|
|
// Check if custom content exists
|
|
|
|
|
|
if (await fs.pathExists(customPath)) {
|
|
|
|
|
|
console.log(chalk.cyan('\n📦 Detecting custom content to update IDE commands...'));
|
|
|
|
|
|
|
|
|
|
|
|
// Get existing custom content selections (default to all for updates)
|
|
|
|
|
|
const existingCustom = {
|
|
|
|
|
|
agents: (await fs.pathExists(path.join(customPath, 'agents'))) ? true : false,
|
|
|
|
|
|
workflows: (await fs.pathExists(path.join(customPath, 'workflows'))) ? true : false,
|
|
|
|
|
|
modules: (await fs.pathExists(path.join(customPath, 'modules'))) ? true : false,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-select all existing custom content for update
|
|
|
|
|
|
if (existingCustom.agents || existingCustom.workflows || existingCustom.modules) {
|
|
|
|
|
|
const customContent = await ui.discoverCustomContent(customPath);
|
|
|
|
|
|
|
|
|
|
|
|
config.customContent = {
|
|
|
|
|
|
path: customPath,
|
|
|
|
|
|
selectedItems: {
|
|
|
|
|
|
agents: existingCustom.agents ? customContent.agents.map((a) => ({ ...a, selected: true })) : [],
|
|
|
|
|
|
workflows: existingCustom.workflows ? customContent.workflows.map((w) => ({ ...w, selected: true })) : [],
|
|
|
|
|
|
modules: existingCustom.modules ? customContent.modules.map((m) => ({ ...m, selected: true })) : [],
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await installCustomContent(config, result, config.directory);
|
|
|
|
|
|
|
|
|
|
|
|
// Re-run IDE setup to register custom workflows with IDEs
|
|
|
|
|
|
if (config.ides && config.ides.length > 0) {
|
|
|
|
|
|
console.log(chalk.cyan('\n🔧 Updating IDE configurations for custom content...'));
|
|
|
|
|
|
const { IdeManager } = require('../installers/lib/ide/manager');
|
|
|
|
|
|
const ideManager = new IdeManager();
|
|
|
|
|
|
|
|
|
|
|
|
for (const ide of config.ides) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const ideResult = await ideManager.setup(ide, config.directory, result.path, {
|
|
|
|
|
|
selectedModules: [...(config.modules || []), 'custom'], // Include 'custom' for custom agents/workflows
|
|
|
|
|
|
skipModuleInstall: true, // Don't install modules again
|
|
|
|
|
|
verbose: false,
|
|
|
|
|
|
preCollectedConfig: {
|
|
|
|
|
|
...config.coreConfig,
|
|
|
|
|
|
_alreadyConfigured: true, // Skip reconfiguration that might add duplicates
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (ideResult.success) {
|
|
|
|
|
|
console.log(chalk.dim(` ✓ Updated ${ide} with custom workflows`));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.log(chalk.yellow(` ⚠️ Could not update ${ide}: ${error.message}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(chalk.dim(' No custom content found to update'));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(chalk.green('\n✨ Update complete with custom content!'));
|
2025-10-26 16:17:37 -05:00
|
|
|
|
process.exit(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 12:47:45 -05:00
|
|
|
|
// Handle reinstall by setting force flag
|
|
|
|
|
|
if (config.actionType === 'reinstall') {
|
|
|
|
|
|
config._requestedReinstall = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 16:47:15 -06:00
|
|
|
|
// Add skip cleanup flag if option provided
|
|
|
|
|
|
if (options && options.skipCleanup) {
|
|
|
|
|
|
config.skipCleanup = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-02 21:45:59 -05:00
|
|
|
|
// Regular install/update flow
|
2025-09-28 23:17:07 -05:00
|
|
|
|
const result = await installer.install(config);
|
|
|
|
|
|
|
2025-10-04 19:46:16 -05:00
|
|
|
|
// Check if installation was cancelled
|
|
|
|
|
|
if (result && result.cancelled) {
|
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if installation succeeded
|
|
|
|
|
|
if (result && result.success) {
|
2025-12-05 22:32:59 -06:00
|
|
|
|
// Install custom content if selected
|
|
|
|
|
|
if (config.customContent && config.customContent.selectedItems) {
|
|
|
|
|
|
console.log(chalk.cyan('\n📦 Installing Custom Content...'));
|
|
|
|
|
|
await installCustomContent(config, result, config.directory);
|
|
|
|
|
|
|
|
|
|
|
|
// Re-run IDE setup to register custom workflows with IDEs
|
|
|
|
|
|
if (config.ides && config.ides.length > 0) {
|
|
|
|
|
|
console.log(chalk.cyan('\n🔧 Updating IDE configurations for custom content...'));
|
|
|
|
|
|
const { IdeManager } = require('../installers/lib/ide/manager');
|
|
|
|
|
|
const ideManager = new IdeManager();
|
|
|
|
|
|
|
|
|
|
|
|
for (const ide of config.ides) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const ideResult = await ideManager.setup(ide, config.directory, result.path, {
|
|
|
|
|
|
selectedModules: [...(config.modules || []), 'custom'], // Include 'custom' for custom agents/workflows
|
|
|
|
|
|
skipModuleInstall: true, // Don't install modules again
|
|
|
|
|
|
verbose: false,
|
|
|
|
|
|
preCollectedConfig: {
|
|
|
|
|
|
...config.coreConfig,
|
|
|
|
|
|
_alreadyConfigured: true, // Skip reconfiguration that might add duplicates
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (ideResult.success) {
|
|
|
|
|
|
console.log(chalk.dim(` ✓ Updated ${ide} with custom workflows`));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.log(chalk.yellow(` ⚠️ Could not update ${ide}: ${error.message}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-04 19:46:16 -05:00
|
|
|
|
console.log(chalk.green('\n✨ Installation complete!'));
|
2025-11-08 13:58:43 -06:00
|
|
|
|
console.log(chalk.cyan('BMAD Core and Selected Modules have been installed to:'), chalk.bold(result.path));
|
2025-10-04 19:46:16 -05:00
|
|
|
|
console.log(chalk.yellow('\nThank you for helping test the early release version of the new BMad Core and BMad Method!'));
|
2025-11-29 23:23:35 -06:00
|
|
|
|
console.log(chalk.cyan('Stable Beta coming soon - please read the full README.md and linked documentation to get started!'));
|
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
|
|
|
|
|
|
|
|
|
|
// Run AgentVibes installer if needed
|
|
|
|
|
|
if (result.needsAgentVibes) {
|
|
|
|
|
|
console.log(chalk.magenta('\n🎙️ AgentVibes TTS Setup'));
|
|
|
|
|
|
console.log(chalk.cyan('AgentVibes provides voice synthesis for BMAD agents with:'));
|
|
|
|
|
|
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
|
|
|
|
|
|
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
|
|
|
|
|
|
|
|
|
|
|
|
const readline = require('node:readline');
|
|
|
|
|
|
const rl = readline.createInterface({
|
|
|
|
|
|
input: process.stdin,
|
|
|
|
|
|
output: process.stdout,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
|
|
rl.question(chalk.green('Press Enter to start AgentVibes installer...'), () => {
|
|
|
|
|
|
rl.close();
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('');
|
|
|
|
|
|
|
|
|
|
|
|
// Run AgentVibes installer
|
|
|
|
|
|
const { execSync } = require('node:child_process');
|
|
|
|
|
|
try {
|
|
|
|
|
|
execSync('npx agentvibes@latest install', {
|
|
|
|
|
|
cwd: result.projectDir,
|
|
|
|
|
|
stdio: 'inherit',
|
|
|
|
|
|
shell: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log(chalk.green('\n✓ AgentVibes installation complete'));
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.log(chalk.yellow('\n⚠ AgentVibes installation was interrupted or failed'));
|
|
|
|
|
|
console.log(chalk.cyan('You can run it manually later with:'));
|
|
|
|
|
|
console.log(chalk.green(` cd ${result.projectDir}`));
|
|
|
|
|
|
console.log(chalk.green(' npx agentvibes install\n'));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-04 19:46:16 -05:00
|
|
|
|
process.exit(0);
|
|
|
|
|
|
}
|
2025-09-28 23:17:07 -05:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// Check if error has a complete formatted message
|
|
|
|
|
|
if (error.fullMessage) {
|
|
|
|
|
|
console.error(error.fullMessage);
|
|
|
|
|
|
if (error.stack) {
|
|
|
|
|
|
console.error('\n' + chalk.dim(error.stack));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Generic error handling for all other errors
|
|
|
|
|
|
console.error(chalk.red('Installation failed:'), error.message);
|
|
|
|
|
|
console.error(chalk.dim(error.stack));
|
|
|
|
|
|
}
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|