diff --git a/docs/custom-agent-installation.md b/docs/custom-agent-installation.md index 15098094..11b6abc1 100644 --- a/docs/custom-agent-installation.md +++ b/docs/custom-agent-installation.md @@ -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 #Direct path to specific agent YAML file or folder - -d, --defaults #Use default values without prompting - -t, --target #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/`. diff --git a/package.json b/package.json index b3031495..3c99b1f6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/modules/bmb/_module-installer/install-config.yaml b/src/modules/bmb/_module-installer/install-config.yaml index 1f80c9f9..85df89c0 100644 --- a/src/modules/bmb/_module-installer/install-config.yaml +++ b/src/modules/bmb/_module-installer/install-config.yaml @@ -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}" diff --git a/src/modules/bmb/docs/agents/simple-agent-architecture.md b/src/modules/bmb/docs/agents/simple-agent-architecture.md index 239d29d7..9d1898b7 100644 --- a/src/modules/bmb/docs/agents/simple-agent-architecture.md +++ b/src/modules/bmb/docs/agents/simple-agent-architecture.md @@ -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: diff --git a/src/modules/bmb/workflows/create-agent/data/info-and-installation-guide.md b/src/modules/bmb/workflows/create-agent/data/info-and-installation-guide.md index 304bbb98..d4ad0f7e 100644 --- a/src/modules/bmb/workflows/create-agent/data/info-and-installation-guide.md +++ b/src/modules/bmb/workflows/create-agent/data/info-and-installation-guide.md @@ -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 diff --git a/tools/cli/commands/agent-install.js b/tools/cli/commands/agent-install.js deleted file mode 100644 index 2e5dca21..00000000 --- a/tools/cli/commands/agent-install.js +++ /dev/null @@ -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} 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 to specific agent YAML file or folder'], - ['-d, --defaults', 'Use default values without prompting'], - ['-t, --destination ', '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); - } - }, -}; diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index 880d9ba1..8335d8ee 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -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]) { diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index 9081ba3b..79fd183d 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -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; }