Files
brachnha-insight/_bmad/bmb/workflows/module/data/module-installer-standards.md
Max e9e6fadb1d fix: ChatBubble crash and DeepSeek API compatibility
- Fix ChatBubble to handle non-string content with String() wrapper
- Fix API route to use generateText for non-streaming requests
- Add @ai-sdk/openai-compatible for non-OpenAI providers (DeepSeek, etc.)
- Use Chat Completions API instead of Responses API for compatible providers
- Update ChatBubble tests and fix component exports to kebab-case
- Remove stale PascalCase ChatBubble.tsx file
2026-01-26 16:55:05 +07:00

9.2 KiB

Module Installer Standards

Purpose: How the _module-installer folder works, including installer.js patterns and platform-specific configuration.


Overview

The _module-installer folder contains optional installation logic for your module. It runs AFTER the IDE installations and can:

  • Create directories specified in module.yaml
  • Copy assets or templates
  • Configure IDE-specific settings
  • Set up platform-specific integrations

When Do You Need an Installer?

Use an Installer When:

  • Creating directories based on user configuration
  • Copying template files to the user's project
  • IDE-specific setup (Claude Code, Windsurf, Cursor, etc.)
  • Platform-specific integrations

Skip the Installer When:

  • Module only provides agents and workflows
  • No file operations needed
  • No IDE-specific configuration

Folder Structure

_module-installer/
├── installer.js                 # Main installer (REQUIRED if folder exists)
└── platform-specifics/          # IDE-specific handlers (optional)
    ├── claude-code.js
    ├── windsurf.js
    ├── cursor.js
    └── ...

installer.js Pattern

Function Signature

/**
 * Module Installer
 *
 * @param {Object} options - Installation options
 * @param {string} options.projectRoot - The root directory of the target project
 * @param {Object} options.config - Module configuration from module.yaml (resolved variables)
 * @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
 * @param {Object} options.logger - Logger instance for output
 * @returns {Promise<boolean>} - Success status (true = success, false = failure)
 */
async function install(options) {
  const { projectRoot, config, installedIDEs, logger } = options;

  try {
    // Installation logic here
    logger.log(chalk.blue('Installing {Module Name}...'));

    // ... your logic ...

    logger.log(chalk.green('✓ {Module Name} installation complete'));
    return true;
  } catch (error) {
    logger.error(chalk.red(`Error installing module: ${error.message}`));
    return false;
  }
}

module.exports = { install };

What You Receive

Parameter Type Description
projectRoot string Absolute path to the user's project root
config object Resolved module.yaml variables
installedIDEs array List of IDE codes installed (e.g., ['claude-code', 'windsurf'])
logger object Logger with .log(), .warn(), .error() methods

The config object contains your module.yaml variables after user input:

// If module.yaml defined:
// project_name:
//   prompt: "What is your project name?"
//   result: "{value}"

config.project_name  // = user's input
config.planning_artifacts  // = resolved path

Common Installation Tasks

1. Create Directories

const fs = require('fs-extra');
const path = require('node:path');

// Create directory from config
if (config['planning_artifacts']) {
  const dirConfig = config['planning_artifacts'].replace('{project-root}/', '');
  const dirPath = path.join(projectRoot, dirConfig);

  if (!(await fs.pathExists(dirPath))) {
    logger.log(chalk.yellow(`Creating directory: ${dirConfig}`));
    await fs.ensureDir(dirPath);
  }
}

2. Copy Assets

const assetsSource = path.join(__dirname, 'assets');
const assetsDest = path.join(projectRoot, 'docs');

if (await fs.pathExists(assetsSource)) {
  await fs.copy(assetsSource, assetsDest);
  logger.log(chalk.green('✓ Copied assets to docs/'));
}

3. IDE-Specific Configuration

// Handle IDE-specific configurations
if (installedIDEs && installedIDEs.length > 0) {
  logger.log(chalk.cyan(`Configuring for IDEs: ${installedIDEs.join(', ')}`));

  for (const ide of installedIDEs) {
    await configureForIDE(ide, projectRoot, config, logger);
  }
}

Platform-Specific Handlers

Pattern

Create files in platform-specifics/{ide-code}.js:

// platform-specifics/claude-code.js

/**
 * Configure module for Claude Code
 */
async function install(options) {
  const { projectRoot, config, logger, platformInfo } = options;

  try {
    // Claude Code specific configuration
    logger.log(chalk.dim('  Configuring Claude Code integration...'));

    // Your logic here

    return true;
  } catch (error) {
    logger.warn(chalk.yellow(`  Warning: ${error.message}`));
    return false;
  }
}

module.exports = { install };

Load from Main Installer

// installer.js
const platformCodes = require(path.join(__dirname, '../../../../tools/cli/lib/platform-codes'));

async function configureForIDE(ide, projectRoot, config, logger) {
  // Validate platform code
  if (!platformCodes.isValidPlatform(ide)) {
    logger.warn(chalk.yellow(`  Unknown platform: '${ide}'. Skipping.`));
    return;
  }

  const platformName = platformCodes.getDisplayName(ide);
  const platformSpecificPath = path.join(__dirname, 'platform-specifics', `${ide}.js`);

  try {
    if (await fs.pathExists(platformSpecificPath)) {
      const platformHandler = require(platformSpecificPath);

      if (typeof platformHandler.install === 'function') {
        await platformHandler.install({ projectRoot, config, logger });
        logger.log(chalk.green(`  ✓ Configured for ${platformName}`));
      }
    }
  } catch (error) {
    logger.warn(chalk.yellow(`  Warning: Could not configure ${platformName}: ${error.message}`));
  }
}

Complete Example: BMM Installer

const fs = require('fs-extra');
const path = require('node:path');
const chalk = require('chalk');
const platformCodes = require(path.join(__dirname, '../../../../tools/cli/lib/platform-codes'));

/**
 * BMM Module Installer
 */
async function install(options) {
  const { projectRoot, config, installedIDEs, logger } = options;

  try {
    logger.log(chalk.blue('🚀 Installing BMM Module...'));

    // Create output directory
    if (config['output_folder']) {
      const outputConfig = config['output_folder'].replace('{project-root}/', '');
      const outputPath = path.join(projectRoot, outputConfig);
      if (!(await fs.pathExists(outputPath))) {
        logger.log(chalk.yellow(`Creating output directory: ${outputConfig}`));
        await fs.ensureDir(outputPath);
      }
    }

    // Create implementation artifacts directory
    if (config['implementation_artifacts']) {
      const storyConfig = config['implementation_artifacts'].replace('{project-root}/', '');
      const storyPath = path.join(projectRoot, storyConfig);
      if (!(await fs.pathExists(storyPath))) {
        logger.log(chalk.yellow(`Creating story directory: ${storyConfig}`));
        await fs.ensureDir(storyPath);
      }
    }

    // IDE-specific configuration
    if (installedIDEs && installedIDEs.length > 0) {
      logger.log(chalk.cyan(`Configuring BMM for IDEs: ${installedIDEs.join(', ')}`));

      for (const ide of installedIDEs) {
        await configureForIDE(ide, projectRoot, config, logger);
      }
    }

    logger.log(chalk.green('✓ BMM Module installation complete'));
    return true;
  } catch (error) {
    logger.error(chalk.red(`Error installing BMM: ${error.message}`));
    return false;
  }
}

async function configureForIDE(ide, projectRoot, config, logger) {
  if (!platformCodes.isValidPlatform(ide)) {
    logger.warn(chalk.yellow(`  Warning: Unknown platform '${ide}'. Skipping.`));
    return;
  }

  const platformSpecificPath = path.join(__dirname, 'platform-specifics', `${ide}.js`);

  try {
    if (await fs.pathExists(platformSpecificPath)) {
      const platformHandler = require(platformSpecificPath);

      if (typeof platformHandler.install === 'function') {
        await platformHandler.install({ projectRoot, config, logger });
      }
    }
  } catch (error) {
    logger.warn(chalk.yellow(`  Warning: Could not load handler for ${ide}: ${error.message}`));
  }
}

module.exports = { install };

Best Practices

DO:

  • Return true for success, false for failure
  • Use chalk for colored output
  • Log what you're doing (create, copy, configure)
  • Handle errors gracefully with try/catch
  • Validate paths before creating directories

DON'T:

  • Assume paths exist — check with fs.pathExists()
  • Overwrite user files without asking
  • Fail silently — log errors
  • Use absolute paths — build from projectRoot

Available Platform Codes

Common IDE codes:

  • claude-code — Anthropic's Claude Code
  • windsurf — Windsurf IDE
  • cursor — Cursor AI IDE
  • vscode — Visual Studio Code

Use platformCodes.isValidPlatform(ide) to validate.


Testing Your Installer

  1. Create a test project
  2. Run bmad install {your-module}
  3. Verify directories are created
  4. Check that config variables are resolved correctly
  5. Test platform-specific handlers

Quick Reference

Task Code Pattern
Create directory await fs.ensureDir(path)
Check if exists await fs.pathExists(path)
Copy files await fs.copy(src, dest)
Log info logger.log(chalk.blue('message'))
Log success logger.log(chalk.green('✓ message'))
Log warning logger.warn(chalk.yellow('warning'))
Log error logger.error(chalk.red('error'))