39 KiB
BMAD Custom Module Installer - Implementation Plan
Document Version: 1.0 Date: 2025-10-19 Status: Planning Phase Owner: CLI Chief (Scott) + BMad
Executive Summary
This document outlines the architecture and implementation plan for a new BMAD CLI tool that enables installation of custom modules from any location. This tool is critical for the future of BMAD as an extensible framework where module authors can create and distribute modules independently of the core BMAD repository.
The Vision
- Core as npm package: Future state where
@bmad/coreis an npm package with CLI tools - Custom modules: Module authors use BMad Builder (BMB) to create standalone modules
- Universal installer: A CLI tool that can install any valid BMAD module from any path
- IDE integration: Compiled agents work with 14+ IDE environments (Codex, Cursor, Windsurf, etc.)
Problem Statement
Current Limitations
The existing bmad install command (tools/cli/commands/install.js) is hardcoded to:
- Discover modules ONLY from
src/modules/directory - Install bundled modules (BMM, BMB, CIS) that ship with the framework
- Cannot handle external/custom modules from arbitrary filesystem locations
Code Reference: tools/cli/installers/lib/modules/manager.js:27
this.modulesSourcePath = getSourcePath('modules'); // Hardcoded to src/modules/
Real-World Use Case
- User has BMD module at
/Users/brianmadison/dev/BMAD-METHOD/bmd(standalone folder) - Module has agents that need compilation (YAML → Markdown with XML)
- Module needs IDE integration (generate commands for Claude Code, etc.)
- Current installer cannot handle this - module must be in
src/modules/to be discovered
Critical Architectural Understanding
Module Structure (SOURCE - What Authors Create)
CORRECT STRUCTURE:
my-custom-module/
├── agents/
│ └── my-agent.agent.yaml ← Required: At least one agent
├── workflows/ ← Optional: Workflow definitions
│ └── my-workflow/
│ ├── README.md
│ └── workflow.yaml
└── _module-installer/ ← Required: Installation configuration
├── install-config.yaml ← REQUIRED: Defines config questions
└── installer.js ← OPTIONAL: Custom install hooks
CRITICAL: NO config.yaml in source!
- The
config.yamlis GENERATED at install time from user answers - Source modules use
_module-installer/install-config.yamlto define questions - The legacy pattern of having
config.yamlin source is being deprecated
Module Structure (INSTALLED - What Gets Generated)
{target-project}/bmad/my-custom-module/
├── agents/
│ └── my-agent.md ← Compiled from .agent.yaml
├── workflows/
│ └── my-workflow/
└── config.yaml ← GENERATED from user answers during install
Key Points:
_module-installer/directory is NOT copied to target (only used during install)- Agents are compiled from YAML to Markdown with XML
config.yamlis generated fresh for each installation
Example: install-config.yaml
Reference: /Users/brianmadison/dev/BMAD-METHOD/src/modules/bmm/_module-installer/install-config.yaml
# Module metadata
code: bmm
name: 'BMM: BMad Method Agile-AI Driven-Development'
default_selected: true
# Optional welcome message
prompt:
- 'Thank you for choosing the BMAD™ Method...'
- 'All paths are relative to project root, with no leading slash.'
# Configuration questions
project_name:
prompt: 'What is the title of your project?'
default: '{directory_name}'
result: '{value}'
user_skill_level:
prompt:
- 'What is your technical experience level?'
default: 'intermediate'
result: '{value}'
single-select:
- value: 'beginner'
label: 'Beginner - New to development'
- value: 'intermediate'
label: 'Intermediate - Familiar with development'
- value: 'expert'
label: 'Expert - Deep technical knowledge'
tech_docs:
prompt: 'Where is Technical Documentation located?'
default: 'docs'
result: '{project-root}/{value}'
How ConfigCollector Uses This:
- Reads
install-config.yamlfrom source module - Builds interactive prompts for each config item
- Collects user answers
- Processes answers with variable substitution (
{value},{project-root}, etc.) - Generates
config.yamlin installed module location
Code Reference: tools/cli/installers/lib/core/config-collector.js:108-122
Current CLI Architecture
Installation Flow (Existing System)
User runs: npm run install:bmad
1. Command Handler (commands/install.js)
├── Prompts for target directory, modules, IDEs
└── Calls Installer.install(config)
2. Installer (installers/lib/core/installer.js)
├── Validates target directory
├── Resolves module dependencies
├── Calls ModuleManager.install() for each module
├── Calls IdeManager.setup() for each IDE
└── Generates manifests
3. ModuleManager (installers/lib/modules/manager.js)
├── Discovers modules from src/modules/ ONLY
├── Copies module files to {target}/bmad/{module}/
├── Compiles agents using YamlXmlBuilder
└── Runs module-specific installer if exists
4. ConfigCollector (installers/lib/core/config-collector.js)
├── Reads _module-installer/install-config.yaml
├── Prompts user for configuration
├── Generates config.yaml in target
5. IdeManager (installers/lib/ide/manager.js)
├── For each selected IDE (codex, windsurf, cursor, etc.)
├── Creates IDE-specific artifacts
│ - Claude Code: .claude/commands/*.md
│ - Windsurf: .windsurf/workflows/*.yaml
│ - Cursor: .cursor/rules/*.txt
└── Runs platform-specific hooks
6. ManifestGenerator (installers/lib/core/manifest-generator.js)
├── manifest.yaml (installation metadata)
├── workflow-manifest.csv (workflow catalog)
├── agent-manifest.csv (agent metadata)
└── files-manifest.csv (file integrity hashes)
Key Components (Reusable for Custom Installer)
Agent Compilation Engine:
tools/cli/lib/yaml-xml-builder.js- YamlXmlBuilder classtools/cli/lib/activation-builder.js- Generates activation blockstools/cli/lib/agent-analyzer.js- Detects required handlerssrc/utility/models/fragments/*.xml- Reusable XML fragments
Installation Infrastructure:
tools/cli/installers/lib/core/config-collector.js- ConfigCollector classtools/cli/installers/lib/ide/manager.js- IdeManager classtools/cli/installers/lib/core/manifest-generator.js- ManifestGenerator classtools/cli/installers/lib/modules/manager.js- ModuleManager class (needs adaptation)
Key Insight: 80% of the code we need already exists! We just need to:
- Create a new command handler
- Adapt ModuleManager to accept external paths
- Wire everything together
Proposed Solution Architecture
New Command: install-module
Purpose: Install a custom module from any filesystem location
Usage:
# Interactive mode
bmad install-module
# Non-interactive mode
bmad install-module \
--source /path/to/custom-module \
--target /path/to/project \
--ides codex,windsurf
# CI/CD mode
bmad install-module \
--source ./my-module \
--target . \
--ides codex \
--non-interactive \
--config-file ./module-config.json
System Architecture
┌──────────────────────────────────────────────────────────────┐
│ NEW: install-module Command │
│ File: tools/cli/commands/install-module.js │
│ │
│ Responsibilities: │
│ - Parse command-line flags │
│ - Prompt for missing information (interactive mode) │
│ - Validate inputs │
│ - Call CustomModuleInstaller │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ NEW: CustomModuleInstaller Class │
│ File: tools/cli/installers/lib/core/custom-module-installer.js│
│ │
│ Responsibilities: │
│ 1. Validate source module structure (ModuleValidator) │
│ 2. Ensure core is installed in target │
│ 3. Collect module configuration (ConfigCollector) │
│ 4. Install module files (ModuleManager) │
│ 5. Compile agents (YamlXmlBuilder) │
│ 6. Generate IDE artifacts (IdeManager) │
│ 7. Update manifests (ManifestGenerator) │
│ 8. Run custom installer hooks (if exists) │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ NEW: ModuleValidator Class │
│ File: tools/cli/installers/lib/core/module-validator.js │
│ │
│ Validates: │
│ ✓ _module-installer/install-config.yaml exists │
│ ✓ At least one agents/*.agent.yaml exists │
│ ✓ Module metadata is valid │
│ ⚠ Warns if legacy config.yaml found in source │
│ ✗ Fails if required structure missing │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ REUSED: Existing Infrastructure │
│ │
│ - ConfigCollector (configuration prompts) │
│ - YamlXmlBuilder (agent compilation) │
│ - IdeManager (IDE integration) │
│ - ManifestGenerator (tracking) │
│ - ModuleManager (file operations) │
└──────────────────────────────────────────────────────────────┘
Detailed Installation Flow
Phase 1: Validation
Input: --source /path/to/custom-module
1. ModuleValidator.validate(sourcePath)
├── Check: _module-installer/install-config.yaml exists
├── Check: agents/ directory exists
├── Check: At least one *.agent.yaml in agents/
├── Parse: install-config.yaml for metadata
│ - Extract: code, name, version
│ - Extract: dependencies (if any)
│ - Extract: core_version requirement
├── Warn: If legacy config.yaml found in source
└── Return: { valid: true/false, errors: [], warnings: [], metadata: {} }
2. If invalid:
├── Display all errors clearly
└── Exit with helpful message + link to module authoring guide
Phase 2: Core Dependency Check
Input: --target /path/to/project
1. Check if core installed:
├── Look for: {target}/bmad/core/
├── Validate: core/config.yaml exists
└── Check version compatibility
2. If core NOT installed:
├── Display message: "Core framework required but not found"
├── Prompt: "Install core framework now? (Y/n)"
├── If yes: Run core installer
│ └── Use existing Installer.installCore() or similar
├── If no: Exit with error
└── After core install: Continue to Phase 3
3. If core installed but incompatible version:
├── Display warning with version mismatch details
├── Prompt: "Continue anyway? (may cause issues)"
└── Respect user choice
Phase 3: Configuration Collection
Input: Module's install-config.yaml
1. ConfigCollector.collectModuleConfig(moduleName, projectDir)
├── Read: {source}/_module-installer/install-config.yaml
├── Display: Module welcome prompt (if defined)
├── Build questions:
│ - Text inputs
│ - Single-select (radio)
│ - Multi-select (checkboxes)
│ - Confirmations
├── Check for existing values:
│ - If module already installed, load existing config
│ - Prompt: "Use existing value or change?"
├── Prompt user interactively (or use --config-file in non-interactive mode)
└── Return: { key: value } answers object
2. Process answers with variable substitution:
├── {value} → actual answer
├── {project-root} → absolute target path
├── {directory_name} → basename of target directory
├── {value:other_key} → reference another config value
└── Return: Final configuration object
3. Store configuration (will be written in Phase 5)
Phase 4: File Installation
Input: Source module path, Target bmad directory
1. ModuleManager.installFromPath(sourcePath, bmadDir, fileTrackingCallback)
├── Determine module name from metadata
├── Create target directory: {bmadDir}/{module-name}/
├── Copy files with filtering:
│ ├── COPY: agents/ (all files)
│ ├── COPY: workflows/ (strip web_bundle sections from workflow.yaml)
│ ├── SKIP: _module-installer/ (not needed in target)
│ ├── SKIP: config.yaml from source (if exists - legacy)
│ ├── SKIP: *.bak files
│ └── SKIP: Agents with localskip="true" (web-only agents)
└── Track all copied files for manifest generation
2. File tracking callback:
└── Store: { path, hash } for each file (for files-manifest.csv)
Phase 5: Agent Compilation
Input: Installed module path
1. For each agents/*.agent.yaml:
├── Read YAML file
├── Check for customize.yaml (sidecar file)
├── Merge if exists: agent.yaml + customize.yaml
├── YamlXmlBuilder.build(agentData, options)
│ - forWebBundle: false (IDE mode)
│ - includeMetadata: true
│ - skipActivation: false
├── AgentAnalyzer.analyze(agentData)
│ - Detect: Which handlers are used (workflow, exec, tmpl, data, action)
├── ActivationBuilder.build(handlers)
│ - Load: activation-steps.xml (base)
│ - Inject: Only needed handler fragments
├── Generate: Markdown file with XML
└── Write: {bmadDir}/{module}/agents/{name}.md
2. Result:
└── Compiled agents ready for IDE consumption
Phase 6: Configuration File Generation
Input: Collected configuration from Phase 3
1. Build config.yaml content:
├── Add: Module metadata (code, name, version)
├── Add: All configuration values from user answers
├── Add: Installation metadata
│ - installed_date
│ - installed_version
└── Add: User info from core config
- user_name
- communication_language
- output_folder
2. Write config.yaml:
└── {bmadDir}/{module}/config.yaml
3. This is the ONLY config.yaml that exists after installation
Phase 7: IDE Integration
Input: Selected IDEs (codex, windsurf, cursor, etc.)
1. IdeManager.setup(selectedIdes, bmadDir, projectRoot)
├── For each IDE:
│ ├── Load IDE handler: ide/{ide-code}.js
│ ├── Call: handler.setup()
│ ├── Call: handler.createArtifacts()
│ │ └── Generate IDE-specific files
│ └── Run: Platform-specific hooks if defined
│ - Check: {source}/_module-installer/platform-specifics/{ide}.js
│ - Execute if exists
└── Examples:
- Claude Code: .claude/commands/bmad/{module}/agents/*.md
- Windsurf: .windsurf/workflows/bmad-{module}-*.yaml
- Cursor: .cursor/rules/bmad-{module}.txt
2. Workflow Command Generation:
├── Read: workflow-manifest.csv (from Phase 8)
├── For each workflow in module:
│ └── Generate: IDE command to launch workflow
└── Format varies by IDE
Phase 8: Manifest Updates
Input: Installation details, installed files, module metadata
1. ManifestGenerator.update(bmadDir, installData)
├── Update: {bmadDir}/_cfg/manifest.yaml
│ - Add module to installed_modules[]
│ - Add custom_modules[] section (track source path)
│ - Update: last_modified timestamp
│
├── Update: {bmadDir}/_cfg/agent-manifest.csv
│ - Add row for each agent
│ - Columns: module, agent_path, agent_name, role, identity_summary,
│ communication_style, expertise, approach, responsibilities, workflows
│
├── Update: {bmadDir}/_cfg/workflow-manifest.csv
│ - Add row for each workflow
│ - Columns: module, workflow_path, workflow_name, description, scale_level
│
├── Update: {bmadDir}/_cfg/files-manifest.csv
│ - Add row for each installed file
│ - Columns: file_path, file_type, module, hash (SHA256)
│
└── Update: {bmadDir}/_cfg/task-manifest.csv (if tasks exist - legacy)
2. Manifest purposes:
- Update detection (compare file hashes)
- Installation integrity validation
- Rollback capability
- IDE artifact generation
- Documentation generation
Phase 9: Custom Installer Hooks
Input: Module's _module-installer/installer.js (if exists)
1. Check for custom installer:
└── {source}/_module-installer/installer.js
2. If exists:
├── Load module: require(installerPath)
├── Validate: exports.install is a function
├── Prepare context:
│ {
│ projectRoot: '/path/to/project',
│ config: { collected user configuration },
│ installedIDEs: ['codex', 'windsurf'],
│ logger: { log, error, warn }
│ }
├── Execute: await installer.install(context)
└── Handle errors gracefully
3. Custom installer use cases:
- Create subagent variations
- Set up additional project files
- Run initialization scripts
- Configure external dependencies
Phase 10: Validation & Completion
1. Validate installation:
├── Check: All manifest files exist
├── Verify: Agent files compiled successfully
├── Verify: IDE artifacts created
├── Validate: File hashes match manifest
└── Check: No errors during installation
2. Display success message:
├── Show: Module name and version
├── Show: Installation location
├── Show: Installed agents count
├── Show: IDE integrations configured
└── Show: Next steps
3. Next steps message:
- How to use the module
- How to verify IDE integration
- Link to module documentation
- How to update or uninstall
Implementation Checklist
New Files to Create
-
tools/cli/commands/install-module.js- Command handler for
bmad install-module - CLI argument parsing
- Interactive prompts for missing info
- Call CustomModuleInstaller
- Command handler for
-
tools/cli/installers/lib/core/custom-module-installer.js- CustomModuleInstaller class
- Main orchestration logic
- Coordinate all phases (1-10)
- Error handling and rollback
-
tools/cli/installers/lib/core/module-validator.js- ModuleValidator class
- Validate module structure
- Check required files
- Parse and validate metadata
- Return detailed validation results
-
tools/cli/installers/lib/core/core-installer.js(optional)- CoreInstaller class
- Install just the core framework
- Can be extracted from existing Installer class
Files to Modify
-
tools/cli/installers/lib/modules/manager.js- Add:
installFromPath(sourcePath, bmadDir, ...)method - Adapt existing
install()logic to work with external paths - Keep existing functionality intact (backward compatibility)
- Add:
-
tools/cli/installers/lib/core/manifest-generator.js- Add: Support for tracking custom module source paths
- Add:
custom_modulessection in manifest.yaml - Format:
custom_modules: - name: my-module source_path: /path/to/source/my-module installed_date: 2025-10-19 version: 1.0.0
-
tools/cli/bmad-cli.js- Already dynamically loads commands, no changes needed
- New command will be auto-discovered
Files to Document
-
docs/custom-module-authoring-guide.md(new)- How to create a custom module
- Required structure and files
- install-config.yaml format
- Best practices
- Testing your module
- Distribution strategies
-
tools/cli/README.md(update)- Add documentation for
install-modulecommand - Update architecture diagrams
- Add examples
- Add documentation for
Testing Strategy
-
Test with existing BMD module
- Source:
/Users/brianmadison/dev/BMAD-METHOD/bmd - Target: Test project
- Validate: All phases work correctly
- Source:
-
Create test fixtures
- Minimal valid module
- Module with all optional features
- Invalid modules (for error testing)
-
IDE integration tests
- Test with Claude Code
- Test with Windsurf
- Verify artifact generation
Code Examples
Example: ModuleValidator.validate()
// tools/cli/installers/lib/core/module-validator.js
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('js-yaml');
class ModuleValidator {
async validate(sourcePath) {
const result = {
valid: false,
errors: [],
warnings: [],
metadata: null,
};
// 1. Check _module-installer/install-config.yaml
const installConfigPath = path.join(sourcePath, '_module-installer', 'install-config.yaml');
if (!(await fs.pathExists(installConfigPath))) {
result.errors.push('Missing required file: _module-installer/install-config.yaml');
} else {
// Parse and validate
try {
const content = await fs.readFile(installConfigPath, 'utf8');
const config = yaml.load(content);
// Extract metadata
result.metadata = {
code: config.code,
name: config.name,
version: config.version || '1.0.0',
dependencies: config.dependencies || [],
core_version: config.core_version,
};
// Validate required metadata
if (!config.code) {
result.errors.push('install-config.yaml missing required field: code');
}
if (!config.name) {
result.errors.push('install-config.yaml missing required field: name');
}
} catch (error) {
result.errors.push(`Invalid install-config.yaml: ${error.message}`);
}
}
// 2. Check agents/ directory
const agentsPath = path.join(sourcePath, 'agents');
if (!(await fs.pathExists(agentsPath))) {
result.errors.push('Missing required directory: agents/');
} else {
const agentFiles = await fs.readdir(agentsPath);
const yamlAgents = agentFiles.filter((f) => f.endsWith('.agent.yaml'));
if (yamlAgents.length === 0) {
result.errors.push('No agent YAML files found in agents/ directory');
} else {
result.metadata = result.metadata || {};
result.metadata.agent_count = yamlAgents.length;
}
}
// 3. Warn about legacy config.yaml
const legacyConfigPath = path.join(sourcePath, 'config.yaml');
if (await fs.pathExists(legacyConfigPath)) {
result.warnings.push(
'Found config.yaml in module source. This is legacy and will be ignored. ' +
'The installer will generate config.yaml from user input. ' +
'Use _module-installer/install-config.yaml instead.',
);
}
// 4. Check for workflows (optional but log if missing)
const workflowsPath = path.join(sourcePath, 'workflows');
if (!(await fs.pathExists(workflowsPath))) {
result.warnings.push('No workflows/ directory found (optional but recommended)');
}
// Set valid flag
result.valid = result.errors.length === 0;
return result;
}
}
module.exports = { ModuleValidator };
Example: CustomModuleInstaller.install()
// tools/cli/installers/lib/core/custom-module-installer.js
const chalk = require('chalk');
const ora = require('ora');
const { ModuleValidator } = require('./module-validator');
const { ConfigCollector } = require('./config-collector');
const { ModuleManager } = require('../modules/manager');
const { IdeManager } = require('../ide/manager');
const { ManifestGenerator } = require('./manifest-generator');
class CustomModuleInstaller {
constructor() {
this.validator = new ModuleValidator();
this.configCollector = new ConfigCollector();
this.moduleManager = new ModuleManager();
this.ideManager = new IdeManager();
this.manifestGenerator = new ManifestGenerator();
}
async install(options) {
const { sourcePath, targetPath, selectedIdes } = options;
console.log(chalk.cyan('\n🔧 BMAD Custom Module Installer\n'));
// PHASE 1: Validate source module
console.log(chalk.bold('Phase 1: Validating module structure...'));
const validation = await this.validator.validate(sourcePath);
if (!validation.valid) {
console.error(chalk.red('\n❌ Module validation failed:\n'));
validation.errors.forEach((err) => console.error(chalk.red(` - ${err}`)));
throw new Error('Invalid module structure');
}
if (validation.warnings.length > 0) {
console.log(chalk.yellow('\n⚠️ Warnings:'));
validation.warnings.forEach((warn) => console.log(chalk.yellow(` - ${warn}`)));
}
console.log(chalk.green('✓ Module structure valid'));
console.log(chalk.dim(` Module: ${validation.metadata.name}`));
console.log(chalk.dim(` Code: ${validation.metadata.code}`));
console.log(chalk.dim(` Agents: ${validation.metadata.agent_count}`));
// PHASE 2: Check core dependency
console.log(chalk.bold('\nPhase 2: Checking core framework...'));
const bmadDir = path.join(targetPath, 'bmad');
const coreInstalled = await this.checkCoreInstalled(bmadDir);
if (!coreInstalled) {
// Prompt to install core
const shouldInstall = await this.promptInstallCore();
if (shouldInstall) {
await this.installCore(targetPath);
} else {
throw new Error('Core framework required for module installation');
}
}
console.log(chalk.green('✓ Core framework available'));
// PHASE 3: Collect configuration
console.log(chalk.bold('\nPhase 3: Collecting module configuration...'));
const config = await this.configCollector.collectModuleConfigFromPath(sourcePath, validation.metadata.code, targetPath);
console.log(chalk.green('✓ Configuration collected'));
// PHASE 4-6: Install module files and compile agents
console.log(chalk.bold('\nPhase 4-6: Installing module and compiling agents...'));
const spinner = ora('Installing module files...').start();
const installResult = await this.moduleManager.installFromPath(sourcePath, bmadDir, (file) => this.trackFile(file), {
moduleConfig: config,
installedIDEs: selectedIdes,
});
spinner.succeed('Module files installed and agents compiled');
// PHASE 7: IDE integration
if (selectedIdes && selectedIdes.length > 0) {
console.log(chalk.bold('\nPhase 7: Configuring IDE integrations...'));
await this.ideManager.setup(selectedIdes, bmadDir, targetPath);
console.log(chalk.green(`✓ Configured ${selectedIdes.length} IDE(s)`));
}
// PHASE 8: Update manifests
console.log(chalk.bold('\nPhase 8: Updating manifests...'));
await this.manifestGenerator.updateForCustomModule({
bmadDir,
moduleName: validation.metadata.code,
sourcePath,
metadata: validation.metadata,
installedFiles: this.trackedFiles,
});
console.log(chalk.green('✓ Manifests updated'));
// PHASE 9: Run custom installer
const customInstallerPath = path.join(sourcePath, '_module-installer', 'installer.js');
if (await fs.pathExists(customInstallerPath)) {
console.log(chalk.bold('\nPhase 9: Running custom installer hooks...'));
await this.runCustomInstaller(customInstallerPath, {
projectRoot: targetPath,
config,
installedIDEs: selectedIdes,
});
console.log(chalk.green('✓ Custom installer completed'));
}
// PHASE 10: Success
console.log(chalk.green('\n✨ Module installation complete!\n'));
console.log(chalk.cyan('Module:'), chalk.bold(validation.metadata.name));
console.log(chalk.cyan('Location:'), path.join(bmadDir, validation.metadata.code));
console.log(chalk.cyan('Agents:'), validation.metadata.agent_count);
if (selectedIdes && selectedIdes.length > 0) {
console.log(chalk.cyan('IDE Integration:'), selectedIdes.join(', '));
}
return { success: true };
}
trackFile(filePath) {
if (!this.trackedFiles) this.trackedFiles = [];
this.trackedFiles.push(filePath);
}
// ... other helper methods
}
module.exports = { CustomModuleInstaller };
Example: ModuleManager.installFromPath()
// Addition to tools/cli/installers/lib/modules/manager.js
/**
* Install a module from an external path (not from src/modules/)
* @param {string} sourcePath - Absolute path to module source
* @param {string} bmadDir - Target bmad directory
* @param {Function} fileTrackingCallback - Optional callback to track files
* @param {Object} options - Installation options
*/
async installFromPath(sourcePath, bmadDir, fileTrackingCallback = null, options = {}) {
// Read module metadata from install-config.yaml
const installConfigPath = path.join(
sourcePath,
'_module-installer',
'install-config.yaml'
);
const configContent = await fs.readFile(installConfigPath, 'utf8');
const config = yaml.load(configContent);
const moduleName = config.code;
const targetPath = path.join(bmadDir, moduleName);
// Check if already installed
if (await fs.pathExists(targetPath)) {
console.log(chalk.yellow(`Module '${moduleName}' already installed, updating...`));
await fs.remove(targetPath);
}
// Copy module files with filtering (reuse existing method)
await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback);
// Process agent files to inject activation block (reuse existing method)
await this.processAgentFiles(targetPath, moduleName);
// Write generated config.yaml
if (options.moduleConfig) {
const configYamlPath = path.join(targetPath, 'config.yaml');
const configYaml = yaml.dump(options.moduleConfig);
await fs.writeFile(configYamlPath, configYaml, 'utf8');
if (fileTrackingCallback) {
fileTrackingCallback(configYamlPath);
}
}
// Call module-specific installer if it exists
if (!options.skipModuleInstaller) {
await this.runModuleInstallerFromPath(sourcePath, bmadDir, options);
}
return {
success: true,
module: moduleName,
path: targetPath,
};
}
/**
* Run module-specific installer from external path
*/
async runModuleInstallerFromPath(sourcePath, bmadDir, options = {}) {
const installerPath = path.join(sourcePath, '_module-installer', 'installer.js');
if (!(await fs.pathExists(installerPath))) {
return; // No custom installer
}
try {
const moduleInstaller = require(installerPath);
if (typeof moduleInstaller.install === 'function') {
const projectRoot = path.dirname(bmadDir);
const logger = options.logger || {
log: console.log,
error: console.error,
warn: console.warn,
};
const result = await moduleInstaller.install({
projectRoot,
config: options.moduleConfig || {},
installedIDEs: options.installedIDEs || [],
logger,
});
if (!result) {
console.warn(chalk.yellow(`Module installer returned false`));
}
}
} catch (error) {
console.error(chalk.red(`Error running module installer: ${error.message}`));
}
}
Command-Line Interface Design
Interactive Mode
$ bmad install-module
🔧 BMAD Custom Module Installer
? Module source path: /Users/brianmadison/dev/my-custom-module
? Target project path: /Users/brianmadison/dev/my-app
? Select IDEs to integrate with: (Use arrows, space to select)
◉ codex (Claude Code)
◯ windsurf (Windsurf)
◯ cursor (Cursor)
◯ cline (Cline)
Validating module structure...
✓ Module structure valid
Module: My Custom Module
Code: my-module
Agents: 3
... (rest of installation)
Non-Interactive Mode
bmad install-module \
--source /path/to/module \
--target /path/to/project \
--ides codex,windsurf \
--non-interactive
With Config File (CI/CD)
# Create config file: module-config.json
{
"project_name": "My Project",
"user_skill_level": "intermediate",
"tech_docs": "docs"
}
# Install with config
bmad install-module \
--source ./my-module \
--target . \
--ides codex \
--config-file ./module-config.json \
--non-interactive
Future Enhancements
npm Package Integration
When core becomes @bmad/core:
# Install globally
npm install -g @bmad/core
# Use anywhere
bmad install-module --source ~/modules/my-module --target ./project
# Or as project dependency
npm install --save-dev @bmad/core
npx bmad install-module --source ./custom-module --target .
Module Registry
Future consideration: BMAD module registry
# Publish to registry
bmad publish-module --source ./my-module
# Install from registry
bmad install-module my-module # Looks up in registry
# Search registry
bmad search-module testing
Update Detection
# Check for updates to custom modules
bmad check-updates
# Update specific module
bmad update-module my-module --from-source /path/to/latest
Testing Plan
Unit Tests
-
ModuleValidator tests
- Valid module structure
- Missing required files
- Invalid metadata
- Legacy warnings
-
ConfigCollector tests
- Read install-config.yaml
- Variable substitution
- Multi-select handling
-
ModuleManager.installFromPath tests
- File copying
- Filtering logic
- Agent compilation
Integration Tests
-
End-to-end installation
- Install BMD module
- Verify all files copied
- Verify agents compiled
- Verify IDE artifacts created
- Verify manifests updated
-
Error scenarios
- Invalid module structure
- Missing core
- Installation failures
- Rollback behavior
Manual Testing
-
Test with BMD module
- Source:
/Users/brianmadison/dev/BMAD-METHOD/bmd - Various IDEs
- Verify functionality
- Source:
-
Test with minimal module
- Create simple test module
- Verify basic flow works
Key Insights & Decisions
Why This Approach?
-
Reuses 80% of existing code: YamlXmlBuilder, IdeManager, ConfigCollector, ManifestGenerator all work as-is
-
Clean separation: New CustomModuleInstaller doesn't interfere with existing Installer
-
Backward compatible: Existing
bmad installcontinues to work unchanged -
Future-proof: Architecture supports npm packaging and module registry
-
Extensible: Easy to add new features like update detection, module search, etc.
Critical Design Principles
- Source modules NEVER have config.yaml - it's generated at install time
- install-config.yaml is the source of truth for module configuration
- _module-installer/ is transient - used during install, not copied to target
- Core is always required - custom modules extend core functionality
- IDE integration is modular - easy to add new IDE support
Common Pitfalls to Avoid
- ❌ Don't copy config.yaml from source
- ❌ Don't skip validation - always validate module structure first
- ❌ Don't ignore legacy warnings - help users modernize
- ❌ Don't forget to update manifests - critical for integrity
- ❌ Don't hardcode paths - use {project-root} placeholders
References
Key Files to Study
- tools/cli/commands/install.js - Current installer command
- tools/cli/installers/lib/core/installer.js - Main installer orchestration
- tools/cli/installers/lib/modules/manager.js - Module management logic
- tools/cli/installers/lib/core/config-collector.js - Configuration collection
- tools/cli/lib/yaml-xml-builder.js - Agent compilation engine
- tools/cli/installers/lib/ide/manager.js - IDE integration
- src/modules/bmm/_module-installer/install-config.yaml - Example config
Documentation
- tools/cli/README.md - CLI documentation
- CLAUDE.md - Project conventions and architecture
- src/modules/bmm/workflows/README.md - BMM workflow guide
Next Steps (When Building)
- Read this document completely
- Study the referenced key files to understand existing patterns
- Start with ModuleValidator - it's the simplest and most isolated
- Then CustomModuleInstaller - wire everything together
- Then command handler - user interface
- Test incrementally - validate each phase works before moving on
- Test with BMD module - real-world validation
- Update documentation - CLI README and create authoring guide
Contact & Support
- Owner: BMad (user_name from config)
- Agent: Scott - Chief CLI Tooling Officer
- Primary Domain: tools/cli/
- Discord: https://discord.gg/gk8jAdXWmj (#general-dev)
- GitHub Issues: https://github.com/bmad-code-org/BMAD-METHOD/issues
Document Status: Ready for implementation Last Updated: 2025-10-19 Version: 1.0