mirror of
https://github.com/bmadcode/BMAD-METHOD.git
synced 2025-12-29 16:14:59 +00:00
custom module installer improved, and removed agent-install
This commit is contained in:
@@ -1,641 +0,0 @@
|
||||
const chalk = require('chalk');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const readline = require('node:readline');
|
||||
const yaml = require('js-yaml');
|
||||
const inquirer = require('inquirer');
|
||||
const {
|
||||
findBmadConfig,
|
||||
resolvePath,
|
||||
discoverAgents,
|
||||
loadAgentConfig,
|
||||
promptInstallQuestions,
|
||||
detectBmadProject,
|
||||
addToManifest,
|
||||
extractManifestData,
|
||||
checkManifestForPath,
|
||||
updateManifestEntry,
|
||||
saveAgentSource,
|
||||
createIdeSlashCommands,
|
||||
updateManifestYaml,
|
||||
} = require('../lib/agent/installer');
|
||||
|
||||
/**
|
||||
* Initialize BMAD core infrastructure in a directory
|
||||
* @param {string} projectDir - Project directory where .bmad should be created
|
||||
* @param {string} bmadFolderName - Name of the BMAD folder (default: .bmad)
|
||||
* @returns {Promise<Object>} BMAD project info
|
||||
*/
|
||||
async function initializeBmadCore(projectDir, bmadFolderName = '.bmad') {
|
||||
const bmadDir = path.join(projectDir, bmadFolderName);
|
||||
const cfgDir = path.join(bmadDir, '_cfg');
|
||||
|
||||
console.log(chalk.cyan('\n🏗️ Initializing BMAD Core Infrastructure\n'));
|
||||
|
||||
// Use the ConfigCollector to ask proper core configuration questions
|
||||
const { ConfigCollector } = require('../installers/lib/core/config-collector');
|
||||
const configCollector = new ConfigCollector();
|
||||
|
||||
// Collect core configuration answers
|
||||
await configCollector.loadExistingConfig(projectDir);
|
||||
await configCollector.collectModuleConfig('core', projectDir, true, true);
|
||||
|
||||
// Extract core answers from allAnswers (they are prefixed with 'core_')
|
||||
const coreAnswers = {};
|
||||
if (configCollector.allAnswers) {
|
||||
for (const [key, value] of Object.entries(configCollector.allAnswers)) {
|
||||
if (key.startsWith('core_')) {
|
||||
const configKey = key.slice(5); // Remove 'core_' prefix
|
||||
coreAnswers[configKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for IDE selection
|
||||
console.log(chalk.cyan('\n💻 IDE Configuration\n'));
|
||||
console.log(chalk.dim('Select IDEs to integrate with the installed agents:'));
|
||||
|
||||
const { UI } = require('../lib/ui');
|
||||
const ui = new UI();
|
||||
const ideConfig = await ui.promptToolSelection(projectDir, ['core']);
|
||||
const selectedIdes = ideConfig.ides || [];
|
||||
|
||||
// Create directory structure
|
||||
console.log(chalk.dim('\nCreating directory structure...'));
|
||||
await fs.promises.mkdir(bmadDir, { recursive: true });
|
||||
await fs.promises.mkdir(cfgDir, { recursive: true });
|
||||
await fs.promises.mkdir(path.join(bmadDir, 'core'), { recursive: true });
|
||||
await fs.promises.mkdir(path.join(bmadDir, 'custom', 'agents'), { recursive: true });
|
||||
await fs.promises.mkdir(path.join(cfgDir, 'agents'), { recursive: true });
|
||||
await fs.promises.mkdir(path.join(cfgDir, 'custom', 'agents'), { recursive: true });
|
||||
|
||||
// Create core config.yaml file
|
||||
const coreConfigFile = {
|
||||
'# CORE Module Configuration': 'Generated by BMAD Agent Installer',
|
||||
Version: require(path.join(__dirname, '../../../package.json')).version,
|
||||
Date: new Date().toISOString(),
|
||||
bmad_folder: bmadFolderName,
|
||||
...coreAnswers,
|
||||
};
|
||||
|
||||
const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
|
||||
await fs.promises.writeFile(coreConfigPath, yaml.dump(coreConfigFile), 'utf8');
|
||||
|
||||
// Create manifest.yaml with complete structure
|
||||
const manifest = {
|
||||
version: require(path.join(__dirname, '../../../package.json')).version,
|
||||
date: new Date().toISOString(),
|
||||
user_name: coreAnswers.user_name,
|
||||
communication_language: coreAnswers.communication_language,
|
||||
document_output_language: coreAnswers.document_output_language,
|
||||
output_folder: coreAnswers.output_folder,
|
||||
install_user_docs: coreAnswers.install_user_docs,
|
||||
bmad_folder: bmadFolderName,
|
||||
modules: ['core'],
|
||||
ides: selectedIdes,
|
||||
custom_agents: [],
|
||||
};
|
||||
|
||||
const manifestPath = path.join(cfgDir, 'manifest.yaml');
|
||||
await fs.promises.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
|
||||
|
||||
// Create empty manifests
|
||||
const agentManifestPath = path.join(cfgDir, 'agent-manifest.csv');
|
||||
await fs.promises.writeFile(agentManifestPath, 'type,subtype,name,path,display_name,description,author,version,tags\n', 'utf8');
|
||||
|
||||
// Setup IDE configurations
|
||||
if (selectedIdes.length > 0) {
|
||||
console.log(chalk.dim('\nSetting up IDE configurations...'));
|
||||
const { IdeManager } = require('../installers/lib/ide/manager');
|
||||
const ideManager = new IdeManager();
|
||||
|
||||
for (const ide of selectedIdes) {
|
||||
await ideManager.setup(ide, projectDir, bmadDir, {
|
||||
selectedModules: ['core'],
|
||||
skipModuleInstall: false,
|
||||
verbose: false,
|
||||
preCollectedConfig: coreAnswers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green('\n✓ BMAD core infrastructure initialized'));
|
||||
console.log(chalk.dim(` BMAD folder: ${bmadDir}`));
|
||||
console.log(chalk.dim(` Core config: ${coreConfigPath}`));
|
||||
console.log(chalk.dim(` Manifest: ${manifestPath}`));
|
||||
if (selectedIdes.length > 0) {
|
||||
console.log(chalk.dim(` IDEs configured: ${selectedIdes.join(', ')}`));
|
||||
}
|
||||
|
||||
return {
|
||||
projectRoot: projectDir,
|
||||
bmadFolder: bmadDir,
|
||||
cfgFolder: cfgDir,
|
||||
manifestFile: agentManifestPath,
|
||||
ides: selectedIdes,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
command: 'agent-install',
|
||||
description: 'Install and compile BMAD agents with personalization',
|
||||
options: [
|
||||
['-s, --source <path>', 'Path to specific agent YAML file or folder'],
|
||||
['-d, --defaults', 'Use default values without prompting'],
|
||||
['-t, --destination <path>', 'Target installation directory (default: current project BMAD installation)'],
|
||||
],
|
||||
action: async (options) => {
|
||||
try {
|
||||
console.log(chalk.cyan('\n🔧 BMAD Agent Installer\n'));
|
||||
|
||||
// Find BMAD config
|
||||
const config = findBmadConfig();
|
||||
if (!config) {
|
||||
console.log(chalk.yellow('No BMAD installation found in current directory.'));
|
||||
console.log(chalk.dim('Looking for .bmad/bmb/config.yaml...'));
|
||||
console.log(chalk.red('\nPlease run this command from a project with BMAD installed.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.dim(`Found BMAD at: ${config.bmadFolder}`));
|
||||
|
||||
let selectedAgent = null;
|
||||
|
||||
// If source provided, use it directly
|
||||
if (options.source) {
|
||||
const providedPath = path.resolve(options.source);
|
||||
|
||||
if (!fs.existsSync(providedPath)) {
|
||||
console.log(chalk.red(`Path not found: ${providedPath}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const stat = fs.statSync(providedPath);
|
||||
if (stat.isFile() && providedPath.endsWith('.agent.yaml')) {
|
||||
selectedAgent = {
|
||||
type: 'simple',
|
||||
name: path.basename(providedPath, '.agent.yaml'),
|
||||
path: providedPath,
|
||||
yamlFile: providedPath,
|
||||
};
|
||||
} else if (stat.isDirectory()) {
|
||||
const yamlFiles = fs.readdirSync(providedPath).filter((f) => f.endsWith('.agent.yaml'));
|
||||
if (yamlFiles.length === 1) {
|
||||
selectedAgent = {
|
||||
type: 'expert',
|
||||
name: path.basename(providedPath),
|
||||
path: providedPath,
|
||||
yamlFile: path.join(providedPath, yamlFiles[0]),
|
||||
hasSidecar: true,
|
||||
};
|
||||
} else {
|
||||
console.log(chalk.red('Directory must contain exactly one .agent.yaml file'));
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.red('Path must be an .agent.yaml file or a folder containing one'));
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Discover agents from custom location
|
||||
const customAgentLocation = config.custom_stand_alone_location
|
||||
? resolvePath(config.custom_stand_alone_location, config)
|
||||
: path.join(config.bmadFolder, 'custom', 'src', 'agents');
|
||||
|
||||
console.log(chalk.dim(`Searching for agents in: ${customAgentLocation}\n`));
|
||||
|
||||
const agents = discoverAgents(customAgentLocation);
|
||||
|
||||
if (agents.length === 0) {
|
||||
console.log(chalk.yellow('No agents found in custom agent location.'));
|
||||
console.log(chalk.dim(`Expected location: ${customAgentLocation}`));
|
||||
console.log(chalk.dim('\nCreate agents using the BMad Builder workflow or place .agent.yaml files there.'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// List available agents
|
||||
console.log(chalk.cyan('Available Agents:\n'));
|
||||
for (const [idx, agent] of agents.entries()) {
|
||||
const typeIcon = agent.type === 'expert' ? '📚' : '📄';
|
||||
console.log(` ${idx + 1}. ${typeIcon} ${chalk.bold(agent.name)} ${chalk.dim(`(${agent.type})`)}`);
|
||||
}
|
||||
|
||||
// Prompt for selection
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const selection = await new Promise((resolve) => {
|
||||
rl.question('\nSelect agent to install (number): ', resolve);
|
||||
});
|
||||
rl.close();
|
||||
|
||||
const selectedIdx = parseInt(selection, 10) - 1;
|
||||
if (isNaN(selectedIdx) || selectedIdx < 0 || selectedIdx >= agents.length) {
|
||||
console.log(chalk.red('Invalid selection'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
selectedAgent = agents[selectedIdx];
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\nSelected: ${chalk.bold(selectedAgent.name)}`));
|
||||
|
||||
// Load agent configuration
|
||||
const agentConfig = loadAgentConfig(selectedAgent.yamlFile);
|
||||
|
||||
// Check if agent has sidecar
|
||||
if (agentConfig.metadata.hasSidecar) {
|
||||
selectedAgent.hasSidecar = true;
|
||||
}
|
||||
|
||||
if (agentConfig.metadata.name) {
|
||||
console.log(chalk.dim(`Agent Name: ${agentConfig.metadata.name}`));
|
||||
}
|
||||
if (agentConfig.metadata.title) {
|
||||
console.log(chalk.dim(`Title: ${agentConfig.metadata.title}`));
|
||||
}
|
||||
if (agentConfig.metadata.hasSidecar) {
|
||||
console.log(chalk.dim(`Sidecar: Yes`));
|
||||
}
|
||||
|
||||
// Get the agent type (source name)
|
||||
const agentType = selectedAgent.name; // e.g., "commit-poet"
|
||||
|
||||
// Confirm/customize agent persona name
|
||||
const rl1 = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const defaultPersonaName = agentConfig.metadata.name || agentType;
|
||||
console.log(chalk.cyan('\n📛 Agent Persona Name\n'));
|
||||
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 && !options.defaults) {
|
||||
answers = await promptInstallQuestions(agentConfig.installConfig, agentConfig.defaults, presetAnswers);
|
||||
} else if (agentConfig.installConfig && options.defaults) {
|
||||
console.log(chalk.dim('\nUsing default configuration values.'));
|
||||
answers = { ...agentConfig.defaults, ...presetAnswers };
|
||||
} else {
|
||||
answers = { ...agentConfig.defaults, ...presetAnswers };
|
||||
}
|
||||
|
||||
// Determine target directory
|
||||
let targetDir = options.destination ? path.resolve(options.destination) : null;
|
||||
|
||||
// If no target specified, prompt for it
|
||||
if (targetDir) {
|
||||
// Check if target has BMAD infrastructure
|
||||
const otherProject = detectBmadProject(targetDir);
|
||||
|
||||
if (!otherProject) {
|
||||
// No BMAD infrastructure found - offer to initialize
|
||||
console.log(chalk.yellow(`\n⚠️ No BMAD infrastructure found in: ${targetDir}`));
|
||||
|
||||
const initResponse = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'initialize',
|
||||
message: 'Initialize BMAD core infrastructure here? (Choose No for direct installation)',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (initResponse.initialize) {
|
||||
// Initialize BMAD core
|
||||
targetDir = path.resolve(targetDir);
|
||||
await initializeBmadCore(targetDir, '.bmad');
|
||||
// Set targetDir to the custom agents folder
|
||||
targetDir = path.join(targetDir, '.bmad', 'custom', 'agents');
|
||||
console.log(chalk.dim(` Agent will be installed to: ${targetDir}`));
|
||||
} else {
|
||||
// User declined - keep original targetDir
|
||||
console.log(chalk.yellow(` Installing agent directly to: ${targetDir}`));
|
||||
}
|
||||
} else if (otherProject && !targetDir.includes('agents')) {
|
||||
console.log(chalk.yellow(`\n⚠️ Path is inside BMAD project: ${otherProject.projectRoot}`));
|
||||
|
||||
const projectChoice = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'choice',
|
||||
message: 'Choose installation method:',
|
||||
choices: [
|
||||
{ name: `Install to BMAD's custom agents folder (${otherProject.bmadFolder}/custom/agents)`, value: 'bmad' },
|
||||
{ name: `Install directly to specified path (${targetDir})`, value: 'direct' },
|
||||
],
|
||||
default: 'bmad',
|
||||
},
|
||||
]);
|
||||
|
||||
if (projectChoice.choice === 'bmad') {
|
||||
targetDir = path.join(otherProject.bmadFolder, 'custom', 'agents');
|
||||
console.log(chalk.dim(` Installing to BMAD custom agents folder: ${targetDir}`));
|
||||
} else {
|
||||
console.log(chalk.yellow(` Installing directly to: ${targetDir}`));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
console.log(chalk.cyan('\n📂 Installation Target\n'));
|
||||
|
||||
// Option 1: Current project's custom agents folder
|
||||
const currentCustom = path.join(config.bmadFolder, 'custom', 'agents');
|
||||
console.log(` 1. Current project: ${chalk.dim(currentCustom)}`);
|
||||
console.log(` 2. Enter path directly (e.g., /Users/brianmadison/dev/test)`);
|
||||
|
||||
const choice = await new Promise((resolve) => {
|
||||
rl.question('\n Select option (1 or 2): ', resolve);
|
||||
});
|
||||
|
||||
if (choice.trim() === '1' || choice.trim() === '') {
|
||||
targetDir = currentCustom;
|
||||
} else if (choice.trim() === '2') {
|
||||
const userPath = await new Promise((resolve) => {
|
||||
rl.question(' Enter path: ', resolve);
|
||||
});
|
||||
|
||||
// Detect if it's a BMAD project and use its custom folder
|
||||
const otherProject = detectBmadProject(path.resolve(userPath));
|
||||
|
||||
if (otherProject) {
|
||||
console.log(chalk.yellow(`\n⚠️ Path is inside BMAD project: ${otherProject.projectRoot}`));
|
||||
|
||||
const projectChoice = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'choice',
|
||||
message: 'Choose installation method:',
|
||||
choices: [
|
||||
{ name: `Install to BMAD's custom agents folder (${otherProject.bmadFolder}/custom/agents)`, value: 'bmad' },
|
||||
{ name: `Install directly to specified path (${userPath})`, value: 'direct' },
|
||||
],
|
||||
default: 'bmad',
|
||||
},
|
||||
]);
|
||||
|
||||
if (projectChoice.choice === 'bmad') {
|
||||
targetDir = path.join(otherProject.bmadFolder, 'custom', 'agents');
|
||||
console.log(chalk.dim(` Installing to BMAD custom agents folder: ${targetDir}`));
|
||||
} else {
|
||||
targetDir = path.resolve(userPath);
|
||||
console.log(chalk.yellow(` Installing directly to: ${targetDir}`));
|
||||
}
|
||||
} else {
|
||||
// No BMAD found - offer to initialize
|
||||
console.log(chalk.yellow(`\n⚠️ No BMAD infrastructure found in: ${userPath}`));
|
||||
|
||||
const initResponse = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'initialize',
|
||||
message: 'Initialize BMAD core infrastructure here? (Choose No for direct installation)',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (initResponse.initialize) {
|
||||
await initializeBmadCore(path.resolve(userPath), '.bmad');
|
||||
targetDir = path.join(path.resolve(userPath), '.bmad', 'custom', 'agents');
|
||||
console.log(chalk.dim(` Agent will be installed to: ${targetDir}`));
|
||||
} else {
|
||||
// User declined - create the directory and install directly
|
||||
targetDir = path.resolve(userPath);
|
||||
console.log(chalk.yellow(` Installing agent directly to: ${targetDir}`));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.red(' Invalid selection. Please choose 1 or 2.'));
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
rl.close();
|
||||
}
|
||||
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(chalk.dim(`\nInstalling to: ${targetDir}`));
|
||||
|
||||
// Detect if target is within a BMAD project
|
||||
const targetProject = detectBmadProject(targetDir);
|
||||
if (targetProject) {
|
||||
console.log(chalk.cyan(` Detected BMAD project at: ${targetProject.projectRoot}`));
|
||||
}
|
||||
|
||||
// Check for duplicate in manifest by path (not by type)
|
||||
let shouldUpdateExisting = false;
|
||||
let existingEntry = null;
|
||||
|
||||
if (targetProject) {
|
||||
// Check if this exact installed name already exists
|
||||
const expectedPath = `.bmad/custom/agents/${finalAgentName}/${finalAgentName}.md`;
|
||||
existingEntry = checkManifestForPath(targetProject.manifestFile, expectedPath);
|
||||
|
||||
if (existingEntry) {
|
||||
const rl2 = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
console.log(chalk.yellow(`\n⚠️ Agent "${finalAgentName}" already installed`));
|
||||
console.log(chalk.dim(` Type: ${agentType}`));
|
||||
console.log(chalk.dim(` Path: ${existingEntry.path}`));
|
||||
|
||||
const overwrite = await new Promise((resolve) => {
|
||||
rl2.question(' Overwrite existing installation? [Y/n]: ', resolve);
|
||||
});
|
||||
rl2.close();
|
||||
|
||||
if (overwrite.toLowerCase() === 'n') {
|
||||
console.log(chalk.yellow('Installation cancelled.'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
shouldUpdateExisting = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Install the agent with custom name
|
||||
// Override the folder name with finalAgentName
|
||||
const agentTargetDir = path.join(targetDir, finalAgentName);
|
||||
|
||||
if (!fs.existsSync(agentTargetDir)) {
|
||||
fs.mkdirSync(agentTargetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Compile and install
|
||||
const { compileAgent } = require('../lib/agent/compiler');
|
||||
|
||||
// Calculate target path for agent ID
|
||||
const projectRoot = targetProject ? targetProject.projectRoot : config.projectRoot;
|
||||
const compiledFileName = `${finalAgentName}.md`;
|
||||
const compiledPath = path.join(agentTargetDir, compiledFileName);
|
||||
const relativePath = path.relative(projectRoot, compiledPath);
|
||||
|
||||
// Read core config to get agent_sidecar_folder
|
||||
const coreConfigPath = path.join(config.bmadFolder, 'bmb', 'config.yaml');
|
||||
let coreConfig = {};
|
||||
if (fs.existsSync(coreConfigPath)) {
|
||||
const yamlLib = require('yaml');
|
||||
const content = fs.readFileSync(coreConfigPath, 'utf8');
|
||||
coreConfig = yamlLib.parse(content);
|
||||
}
|
||||
|
||||
// Compile with proper name and path
|
||||
const { xml, metadata, processedYaml } = compileAgent(
|
||||
fs.readFileSync(selectedAgent.yamlFile, 'utf8'),
|
||||
answers,
|
||||
finalAgentName,
|
||||
relativePath,
|
||||
{ config: coreConfig },
|
||||
);
|
||||
|
||||
// Write compiled XML (.md) with custom name
|
||||
fs.writeFileSync(compiledPath, xml, 'utf8');
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
agentName: finalAgentName,
|
||||
targetDir: agentTargetDir,
|
||||
compiledFile: compiledPath,
|
||||
sidecarCopied: false,
|
||||
};
|
||||
|
||||
// Handle sidecar files for agents with hasSidecar flag
|
||||
if (selectedAgent.hasSidecar === true && selectedAgent.type === 'expert') {
|
||||
const { copyAgentSidecarFiles } = require('../lib/agent/installer');
|
||||
|
||||
// Get agent sidecar folder from config or use default
|
||||
const agentSidecarFolder = coreConfig?.agent_sidecar_folder || '{project-root}/.myagent-data';
|
||||
|
||||
// Resolve path variables
|
||||
const resolvedSidecarFolder = agentSidecarFolder
|
||||
.replaceAll('{project-root}', projectRoot)
|
||||
.replaceAll('{bmad_folder}', config.bmadFolder);
|
||||
|
||||
// Create sidecar directory for this agent
|
||||
const agentSidecarDir = path.join(resolvedSidecarFolder, finalAgentName);
|
||||
if (!fs.existsSync(agentSidecarDir)) {
|
||||
fs.mkdirSync(agentSidecarDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Find and copy sidecar folder
|
||||
const sidecarFiles = copyAgentSidecarFiles(selectedAgent.path, agentSidecarDir, selectedAgent.yamlFile);
|
||||
result.sidecarCopied = true;
|
||||
result.sidecarFiles = sidecarFiles;
|
||||
result.sidecarDir = agentSidecarDir;
|
||||
|
||||
console.log(chalk.dim(` Sidecar copied to: ${agentSidecarDir}`));
|
||||
}
|
||||
|
||||
console.log(chalk.green('\n✨ Agent installed successfully!'));
|
||||
console.log(chalk.cyan(` Name: ${result.agentName}`));
|
||||
console.log(chalk.cyan(` Location: ${result.targetDir}`));
|
||||
console.log(chalk.cyan(` Compiled: ${path.basename(result.compiledFile)}`));
|
||||
|
||||
if (result.sidecarCopied) {
|
||||
console.log(chalk.cyan(` Sidecar files: ${result.sidecarFiles.length} files copied`));
|
||||
}
|
||||
|
||||
// Save source YAML to _cfg/custom/agents/ and register in manifest
|
||||
if (targetProject) {
|
||||
// Save source for reinstallation with embedded answers
|
||||
console.log(chalk.dim(`\nSaving source to: ${targetProject.cfgFolder}/custom/agents/`));
|
||||
saveAgentSource(selectedAgent, targetProject.cfgFolder, finalAgentName, answers);
|
||||
console.log(chalk.green(` ✓ Source saved for reinstallation`));
|
||||
|
||||
// Register/update in manifest
|
||||
console.log(chalk.dim(`Registering in manifest: ${targetProject.manifestFile}`));
|
||||
|
||||
const manifestData = extractManifestData(xml, { ...metadata, name: finalAgentName }, relativePath, 'custom');
|
||||
// Use finalAgentName as the manifest name field (unique identifier)
|
||||
manifestData.name = finalAgentName;
|
||||
// Use compiled metadata.name (persona name after template processing), not source agentConfig
|
||||
manifestData.displayName = metadata.name || agentType;
|
||||
// Store the actual installed path/name
|
||||
manifestData.path = relativePath;
|
||||
|
||||
if (shouldUpdateExisting && existingEntry) {
|
||||
updateManifestEntry(targetProject.manifestFile, manifestData, existingEntry._lineNumber);
|
||||
console.log(chalk.green(` ✓ Updated existing entry in agent-manifest.csv`));
|
||||
} else {
|
||||
addToManifest(targetProject.manifestFile, manifestData);
|
||||
console.log(chalk.green(` ✓ Added to agent-manifest.csv`));
|
||||
}
|
||||
|
||||
// Create IDE slash commands
|
||||
const ideResults = await createIdeSlashCommands(targetProject.projectRoot, finalAgentName, relativePath, metadata);
|
||||
if (Object.keys(ideResults).length > 0) {
|
||||
console.log(chalk.green(` ✓ Created IDE commands:`));
|
||||
for (const [ideName, result] of Object.entries(ideResults)) {
|
||||
console.log(chalk.dim(` ${ideName}: ${result.command}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Update manifest.yaml with custom_agents tracking
|
||||
const manifestYamlPath = path.join(targetProject.cfgFolder, 'manifest.yaml');
|
||||
if (updateManifestYaml(manifestYamlPath, finalAgentName, agentType)) {
|
||||
console.log(chalk.green(` ✓ Updated manifest.yaml custom_agents`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.dim(`\nAgent ID: ${relativePath}`));
|
||||
|
||||
if (targetProject) {
|
||||
console.log(chalk.yellow('\nAgent is now registered and available in the target project!'));
|
||||
} else {
|
||||
console.log(chalk.yellow('\nTo use this agent, reference it in your manifest or load it directly.'));
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Agent installation failed:'), error.message);
|
||||
console.error(chalk.dim(error.stack));
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -198,9 +198,27 @@ class ConfigCollector {
|
||||
}
|
||||
|
||||
let configPath = null;
|
||||
let isCustomModule = false;
|
||||
|
||||
if (await fs.pathExists(installerConfigPath)) {
|
||||
configPath = installerConfigPath;
|
||||
} else {
|
||||
// Check if this is a custom module with custom.yaml
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
const moduleManager = new ModuleManager();
|
||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||
|
||||
if (moduleSourcePath) {
|
||||
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
|
||||
const moduleInstallerCustomPath = path.join(moduleSourcePath, '_module-installer', 'custom.yaml');
|
||||
|
||||
if ((await fs.pathExists(rootCustomConfigPath)) || (await fs.pathExists(moduleInstallerCustomPath))) {
|
||||
isCustomModule = true;
|
||||
// For custom modules, we don't have an install-config schema, so just use existing values
|
||||
// The custom.yaml values will be loaded and merged during installation
|
||||
}
|
||||
}
|
||||
|
||||
// No config schema for this module - use existing values
|
||||
if (this.existingConfig && this.existingConfig[moduleName]) {
|
||||
if (!this.collectedConfig[moduleName]) {
|
||||
|
||||
@@ -378,6 +378,35 @@ class ModuleManager {
|
||||
throw new Error(`Module '${moduleName}' not found in any source location`);
|
||||
}
|
||||
|
||||
// Check if this is a custom module and read its custom.yaml values
|
||||
let customConfig = null;
|
||||
const rootCustomConfigPath = path.join(sourcePath, 'custom.yaml');
|
||||
const moduleInstallerCustomPath = path.join(sourcePath, '_module-installer', 'custom.yaml');
|
||||
|
||||
if (await fs.pathExists(rootCustomConfigPath)) {
|
||||
try {
|
||||
const customContent = await fs.readFile(rootCustomConfigPath, 'utf8');
|
||||
customConfig = yaml.load(customContent);
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message));
|
||||
}
|
||||
} else if (await fs.pathExists(moduleInstallerCustomPath)) {
|
||||
try {
|
||||
const customContent = await fs.readFile(moduleInstallerCustomPath, 'utf8');
|
||||
customConfig = yaml.load(customContent);
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message));
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a custom module, merge its values into the module config
|
||||
if (customConfig) {
|
||||
options.moduleConfig = { ...options.moduleConfig, ...customConfig };
|
||||
if (options.logger) {
|
||||
options.logger.log(chalk.cyan(` Merged custom configuration for ${moduleName}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already installed
|
||||
if (await fs.pathExists(targetPath)) {
|
||||
console.log(chalk.yellow(`Module '${moduleName}' already installed, updating...`));
|
||||
@@ -552,8 +581,8 @@ class ModuleManager {
|
||||
}
|
||||
|
||||
// Skip config.yaml templates - we'll generate clean ones with actual values
|
||||
// But allow custom.yaml which is used for custom modules
|
||||
if ((file === 'config.yaml' || file.endsWith('/config.yaml')) && !file.endsWith('custom.yaml')) {
|
||||
// Also skip custom.yaml files - their values will be merged into core config
|
||||
if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user