Files
BMAD-METHOD/bmd/bmad-custom-module-installer-plan.md

39 KiB
Raw Permalink Blame History

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/core is 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.yaml is GENERATED at install time from user answers
  • Source modules use _module-installer/install-config.yaml to define questions
  • The legacy pattern of having config.yaml in 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.yaml is 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:

  1. Reads install-config.yaml from source module
  2. Builds interactive prompts for each config item
  3. Collects user answers
  4. Processes answers with variable substitution ({value}, {project-root}, etc.)
  5. Generates config.yaml in 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 class
  • tools/cli/lib/activation-builder.js - Generates activation blocks
  • tools/cli/lib/agent-analyzer.js - Detects required handlers
  • src/utility/models/fragments/*.xml - Reusable XML fragments

Installation Infrastructure:

  • tools/cli/installers/lib/core/config-collector.js - ConfigCollector class
  • tools/cli/installers/lib/ide/manager.js - IdeManager class
  • tools/cli/installers/lib/core/manifest-generator.js - ManifestGenerator class
  • tools/cli/installers/lib/modules/manager.js - ModuleManager class (needs adaptation)

Key Insight: 80% of the code we need already exists! We just need to:

  1. Create a new command handler
  2. Adapt ModuleManager to accept external paths
  3. 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

  1. tools/cli/commands/install-module.js

    • Command handler for bmad install-module
    • CLI argument parsing
    • Interactive prompts for missing info
    • Call CustomModuleInstaller
  2. tools/cli/installers/lib/core/custom-module-installer.js

    • CustomModuleInstaller class
    • Main orchestration logic
    • Coordinate all phases (1-10)
    • Error handling and rollback
  3. tools/cli/installers/lib/core/module-validator.js

    • ModuleValidator class
    • Validate module structure
    • Check required files
    • Parse and validate metadata
    • Return detailed validation results
  4. 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

  1. 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)
  2. tools/cli/installers/lib/core/manifest-generator.js

    • Add: Support for tracking custom module source paths
    • Add: custom_modules section 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
      
  3. tools/cli/bmad-cli.js

    • Already dynamically loads commands, no changes needed
    • New command will be auto-discovered

Files to Document

  1. 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
  2. tools/cli/README.md (update)

    • Add documentation for install-module command
    • Update architecture diagrams
    • Add examples

Testing Strategy

  1. Test with existing BMD module

    • Source: /Users/brianmadison/dev/BMAD-METHOD/bmd
    • Target: Test project
    • Validate: All phases work correctly
  2. Create test fixtures

    • Minimal valid module
    • Module with all optional features
    • Invalid modules (for error testing)
  3. 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

  1. ModuleValidator tests

    • Valid module structure
    • Missing required files
    • Invalid metadata
    • Legacy warnings
  2. ConfigCollector tests

    • Read install-config.yaml
    • Variable substitution
    • Multi-select handling
  3. ModuleManager.installFromPath tests

    • File copying
    • Filtering logic
    • Agent compilation

Integration Tests

  1. End-to-end installation

    • Install BMD module
    • Verify all files copied
    • Verify agents compiled
    • Verify IDE artifacts created
    • Verify manifests updated
  2. Error scenarios

    • Invalid module structure
    • Missing core
    • Installation failures
    • Rollback behavior

Manual Testing

  1. Test with BMD module

    • Source: /Users/brianmadison/dev/BMAD-METHOD/bmd
    • Various IDEs
    • Verify functionality
  2. Test with minimal module

    • Create simple test module
    • Verify basic flow works

Key Insights & Decisions

Why This Approach?

  1. Reuses 80% of existing code: YamlXmlBuilder, IdeManager, ConfigCollector, ManifestGenerator all work as-is

  2. Clean separation: New CustomModuleInstaller doesn't interfere with existing Installer

  3. Backward compatible: Existing bmad install continues to work unchanged

  4. Future-proof: Architecture supports npm packaging and module registry

  5. Extensible: Easy to add new features like update detection, module search, etc.

Critical Design Principles

  1. Source modules NEVER have config.yaml - it's generated at install time
  2. install-config.yaml is the source of truth for module configuration
  3. _module-installer/ is transient - used during install, not copied to target
  4. Core is always required - custom modules extend core functionality
  5. IDE integration is modular - easy to add new IDE support

Common Pitfalls to Avoid

  1. Don't copy config.yaml from source
  2. Don't skip validation - always validate module structure first
  3. Don't ignore legacy warnings - help users modernize
  4. Don't forget to update manifests - critical for integrity
  5. Don't hardcode paths - use {project-root} placeholders

References

Key Files to Study

  1. tools/cli/commands/install.js - Current installer command
  2. tools/cli/installers/lib/core/installer.js - Main installer orchestration
  3. tools/cli/installers/lib/modules/manager.js - Module management logic
  4. tools/cli/installers/lib/core/config-collector.js - Configuration collection
  5. tools/cli/lib/yaml-xml-builder.js - Agent compilation engine
  6. tools/cli/installers/lib/ide/manager.js - IDE integration
  7. src/modules/bmm/_module-installer/install-config.yaml - Example config

Documentation

  1. tools/cli/README.md - CLI documentation
  2. CLAUDE.md - Project conventions and architecture
  3. src/modules/bmm/workflows/README.md - BMM workflow guide

Next Steps (When Building)

  1. Read this document completely
  2. Study the referenced key files to understand existing patterns
  3. Start with ModuleValidator - it's the simplest and most isolated
  4. Then CustomModuleInstaller - wire everything together
  5. Then command handler - user interface
  6. Test incrementally - validate each phase works before moving on
  7. Test with BMD module - real-world validation
  8. Update documentation - CLI README and create authoring guide

Contact & Support


Document Status: Ready for implementation Last Updated: 2025-10-19 Version: 1.0