mirror of
https://github.com/bmadcode/BMAD-METHOD.git
synced 2025-12-17 09:45:25 +00:00
custom module installer improved, and removed agent-install
This commit is contained in:
parent
b252778043
commit
119187a1e7
@ -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/`.
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user