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 # 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 ## 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 ```bash
# From your project directory with BMAD installed npx bmad-method install
npx bmad-method agent-install
``` ```
Or if you have bmad-cli installed globally: Or if you have bmad-cli installed globally:
```bash ```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 ## What It Does
1. **Discovers** available agent templates from your custom agents folder 1. **Discovers** available agents and workflows from folders with `custom.yaml`
2. **Prompts** you to personalize the agent (name, behavior, preferences) 2. **Installs** to your project's `.bmad/custom/` directory
3. **Compiles** the agent with your choices baked in 3. **Creates** IDE commands for all your configured IDEs (Claude Code, Codex, Cursor, etc.)
4. **Installs** to your project's `.bmad/custom/agents/` directory 4. **Registers** the agent/workflow in the BMAD system
5. **Creates** IDE commands for all your configured IDEs (Claude Code, Codex, Cursor, etc.)
6. **Saves** your configuration for automatic reinstallation during BMAD updates
## Options ## Example custom.yaml
```bash ```yaml
bmad agent-install [options] code: my-custom-agent
name: 'My Custom Agent'
Options: default_selected: true
-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
``` ```
## 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 ## Installing Reference Agents
The BMAD source includes example agents you can install. **You must copy them to your project first.** 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 ```bash
# From your project root # 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 \ 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): **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/ .bmad/custom/agents/
``` ```
### Step 2: Install and Personalize ### Step 2: Create custom.yaml
```bash ```bash
npx bmad-method agent-install # In the agent folder, create custom.yaml
# or: bmad agent-install (if BMAD installed locally) 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: The installer will:
1. Find the copied template in `.bmad/custom/agents/` 1. Find the agent with its `custom.yaml`
2. Prompt for personalization (name, behavior, preferences) 2. Install it to the appropriate location
3. Compile and install with your choices baked in 3. Create IDE commands for immediate use
4. Create IDE commands for immediate use
### Available Reference Agents ### Available Reference Agents
@ -180,4 +134,4 @@ src/modules/bmb/reference/agents/
## Creating Your Own ## 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" "bmad-method": "tools/bmad-npx-wrapper.js"
}, },
"scripts": { "scripts": {
"bmad:agent-install": "node tools/cli/bmad-cli.js agent-install",
"bmad:install": "node tools/cli/bmad-cli.js install", "bmad:install": "node tools/cli/bmad-cli.js install",
"bmad:status": "node tools/cli/bmad-cli.js status", "bmad:status": "node tools/cli/bmad-cli.js status",
"bundle": "node tools/cli/bundlers/bundle-web.js all", "bundle": "node tools/cli/bundlers/bundle-web.js all",

View File

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

View File

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

View File

@ -2,12 +2,24 @@
## Installation ## Installation
```bash Create a `custom.yaml` file in the agent folder:
# Quick install (interactive)
npx bmad-method agent-install --source ./{agent_filename}.agent.yaml
# Quick install (non-interactive) ```yaml
npx bmad-method agent-install --source ./{agent_filename}.agent.yaml --defaults 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 ## 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 configPath = null;
let isCustomModule = false;
if (await fs.pathExists(installerConfigPath)) { if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath; configPath = installerConfigPath;
} else { } 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 // No config schema for this module - use existing values
if (this.existingConfig && this.existingConfig[moduleName]) { if (this.existingConfig && this.existingConfig[moduleName]) {
if (!this.collectedConfig[moduleName]) { if (!this.collectedConfig[moduleName]) {

View File

@ -378,6 +378,35 @@ class ModuleManager {
throw new Error(`Module '${moduleName}' not found in any source location`); 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 // Check if already installed
if (await fs.pathExists(targetPath)) { if (await fs.pathExists(targetPath)) {
console.log(chalk.yellow(`Module '${moduleName}' already installed, updating...`)); 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 // Skip config.yaml templates - we'll generate clean ones with actual values
// But allow custom.yaml which is used for custom modules // Also skip custom.yaml files - their values will be merged into core config
if ((file === 'config.yaml' || file.endsWith('/config.yaml')) && !file.endsWith('custom.yaml')) { if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) {
continue; continue;
} }