custom module installer improved, and removed agent-install

This commit is contained in:
Brian Madison 2025-12-07 02:10:03 -06:00
parent b252778043
commit 119187a1e7
8 changed files with 140 additions and 765 deletions

View File

@ -1,125 +1,68 @@
# Custom Agent Installation
Install and personalize BMAD agents in your project.
BMAD agents and workflows are now installed through the main CLI installer using a `custom.yaml` configuration file or by having an installer file.
## Quick Start
Create a `custom.yaml` file in the root of your agent/workflow folder:
```yaml
code: my-custom-agent
name: 'My Custom Agent'
default_selected: true
```
Then run the BMAD installer from your project directory:
```bash
# From your project directory with BMAD installed
npx bmad-method agent-install
npx bmad-method install
```
Or if you have bmad-cli installed globally:
```bash
bmad agent-install
bmad install
```
## Installation Methods
### Method 1: Stand-alone Folder with custom.yaml
Place your agent or workflow in a folder with a `custom.yaml` file at the root:
```
my-agent/
├── custom.yaml # Required configuration file
├── my-agent.agent.yaml
└── sidecar/ # Optional
└── instructions.md
```
### Method 2: Installer File
For more complex installations, include an `installer.js` or `installer.yaml` file in your agent/workflow folder:
```
my-workflow/
├── workflow.md
└── installer.yaml # Custom installation logic
```
## What It Does
1. **Discovers** available agent templates from your custom agents folder
2. **Prompts** you to personalize the agent (name, behavior, preferences)
3. **Compiles** the agent with your choices baked in
4. **Installs** to your project's `.bmad/custom/agents/` directory
5. **Creates** IDE commands for all your configured IDEs (Claude Code, Codex, Cursor, etc.)
6. **Saves** your configuration for automatic reinstallation during BMAD updates
1. **Discovers** available agents and workflows from folders with `custom.yaml`
2. **Installs** to your project's `.bmad/custom/` directory
3. **Creates** IDE commands for all your configured IDEs (Claude Code, Codex, Cursor, etc.)
4. **Registers** the agent/workflow in the BMAD system
## Options
## Example custom.yaml
```bash
bmad agent-install [options]
Options:
-p, --path <path> #Direct path to specific agent YAML file or folder
-d, --defaults #Use default values without prompting
-t, --target <path> #Target installation directory
```yaml
code: my-custom-agent
name: 'My Custom Agent'
default_selected: true
```
## Installing from Custom Locations
Use the `-s` / `--source` option to install agents from any location:
```bash
# Install agent from a custom folder (expert agent with sidecar)
bmad agent-install -s path/to/my-agent
# Install a specific .agent.yaml file (simple agent)
bmad agent-install -s path/to/my-agent.agent.yaml
# Install with defaults (non-interactive)
bmad agent-install -s path/to/my-agent -d
# Install to a specific destination project
bmad agent-install -s path/to/my-agent --destination /path/to/destination/project
```
This is useful when:
- Your agent is in a non-standard location (not in `.bmad/custom/agents/`)
- You're developing an agent outside the project structure
- You want to install from an absolute path
## Example Session
```
🔧 BMAD Agent Installer
Found BMAD at: /project/.bmad
Searching for agents in: /project/.bmad/custom/agents
Available Agents:
1. 📄 commit-poet (simple)
2. 📚 journal-keeper (expert)
Select agent to install (number): 1
Selected: commit-poet
📛 Agent Persona Name
Agent type: commit-poet
Default persona: Inkwell Von Comitizen
Custom name (or Enter for default): Fred
Persona: Fred
File: fred-commit-poet.md
📝 Agent Configuration
What's your preferred default commit message style?
* 1. Conventional (feat/fix/chore)
2. Narrative storytelling
3. Poetic haiku
4. Detailed explanation
Choice (default: 1): 1
How enthusiastic should the agent be?
1. Moderate - Professional with personality
* 2. High - Genuinely excited
3. EXTREME - Full theatrical drama
Choice (default: 2): 3
Include emojis in commit messages? [Y/n]: y
✨ Agent installed successfully!
Name: fred-commit-poet
Location: /project/.bmad/custom/agents/fred-commit-poet
Compiled: fred-commit-poet.md
✓ Source saved for reinstallation
✓ Added to agent-manifest.csv
✓ Created IDE commands:
claude-code: /bmad:custom:agents:fred-commit-poet
codex: /bmad-custom-agents-fred-commit-poet
github-copilot: bmad-agent-custom-fred-commit-poet
```
## Reinstallation
Custom agents are automatically reinstalled when you run `bmad init --quick`. Your personalization choices are preserved in `.bmad/_cfg/custom/agents/`.
## Installing Reference Agents
The BMAD source includes example agents you can install. **You must copy them to your project first.**
@ -130,8 +73,9 @@ The BMAD source includes example agents you can install. **You must copy them to
```bash
# From your project root
mkdir -p .bmad/custom/agents/my-agent
cp node_modules/bmad-method/src/modules/bmb/reference/agents/stand-alone/commit-poet.agent.yaml \
.bmad/custom/agents/
.bmad/custom/agents/my-agent/
```
**For expert agents** (folder with sidecar files):
@ -142,19 +86,29 @@ cp -r node_modules/bmad-method/src/modules/bmb/reference/agents/agent-with-memor
.bmad/custom/agents/
```
### Step 2: Install and Personalize
### Step 2: Create custom.yaml
```bash
npx bmad-method agent-install
# or: bmad agent-install (if BMAD installed locally)
# In the agent folder, create custom.yaml
cat > .bmad/custom/agents/my-agent/custom.yaml << EOF
code: my-agent
name: "My Custom Agent"
default_selected: true
EOF
```
### Step 3: Install
```bash
npx bmad-method install
# or: bmad install (if BMAD installed locally)
```
The installer will:
1. Find the copied template in `.bmad/custom/agents/`
2. Prompt for personalization (name, behavior, preferences)
3. Compile and install with your choices baked in
4. Create IDE commands for immediate use
1. Find the agent with its `custom.yaml`
2. Install it to the appropriate location
3. Create IDE commands for immediate use
### Available Reference Agents
@ -180,4 +134,4 @@ src/modules/bmb/reference/agents/
## Creating Your Own
Use the BMB agent builder to craft your agents. Once ready to use yourself, place your `.agent.yaml` files or folder in `.bmad/custom/agents/`.
Use the BMB agent builder to craft your agents. Once ready to use, place your `.agent.yaml` files or folders with `custom.yaml` in `.bmad/custom/agents/` or `.bmad/custom/workflows/`.

View File

@ -24,7 +24,6 @@
"bmad-method": "tools/bmad-npx-wrapper.js"
},
"scripts": {
"bmad:agent-install": "node tools/cli/bmad-cli.js agent-install",
"bmad:install": "node tools/cli/bmad-cli.js install",
"bmad:status": "node tools/cli/bmad-cli.js status",
"bundle": "node tools/cli/bundlers/bundle-web.js all",

View File

@ -22,5 +22,5 @@ custom_stand_alone_location:
custom_module_location:
prompt: "Where do custom modules get stored?"
default: "bmad-custom-modules-src/modules"
default: "bmad-custom-modules-src"
result: "{project-root}/{value}"

View File

@ -217,9 +217,13 @@ Features demonstrated:
# Copy to your project
cp /path/to/commit-poet.agent.yaml .bmad/custom/agents/
# Install with personalization
bmad agent-install
# or: npx bmad-method agent-install
# Create custom.yaml and install
echo "code: my-agent
name: My Agent
default_selected: true" > custom.yaml
npx bmad-method install
# or: bmad install
```
The installer:

View File

@ -2,12 +2,24 @@
## Installation
```bash
# Quick install (interactive)
npx bmad-method agent-install --source ./{agent_filename}.agent.yaml
Create a `custom.yaml` file in the agent folder:
# Quick install (non-interactive)
npx bmad-method agent-install --source ./{agent_filename}.agent.yaml --defaults
```yaml
code: { agent_code }
name: '{agent_name}'
default_selected: true
```
Then run:
```bash
npx bmad-method install
```
Or if you have bmad-cli installed globally:
```bash
bmad install
```
## About This Agent

View File

@ -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);
}
},
};

View File

@ -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]) {

View File

@ -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;
}