This commit is contained in:
Nikita Levyankov 2025-12-18 04:25:20 +09:00 committed by GitHub
commit 7d705aa08b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1749 additions and 8 deletions

View File

@ -79,6 +79,8 @@ With **BMad Builder**, you can architect both simple agents and vastly complex d
### 1. Install BMad Method ### 1. Install BMad Method
#### Interactive Installation (Default)
```bash ```bash
# Install v6 Alpha (recommended) # Install v6 Alpha (recommended)
npx bmad-method@alpha install npx bmad-method@alpha install
@ -87,6 +89,65 @@ npx bmad-method@alpha install
npx bmad-method install npx bmad-method install
``` ```
#### Non-Interactive Installation (CI/CD, Automation)
For automated deployments and CI/CD pipelines:
```bash
# Minimal installation with defaults
npx bmad-method@alpha install -y
# Custom configuration
npx bmad-method@alpha install -y \
--user-name=Alice \
--skill-level=advanced \
--output-folder=.bmad-output
# Team-based installation (fullstack team)
npx bmad-method@alpha install -y --team=fullstack
# Team with modifications
npx bmad-method@alpha install -y --team=fullstack --agents=+dev
# Selective agents and workflows
npx bmad-method@alpha install -y \
--agents=dev,architect,pm \
--workflows=create-prd,create-tech-spec,dev-story
# Profile-based installation
npx bmad-method@alpha install -y --profile=minimal
npx bmad-method@alpha install -y --profile=solo-dev
```
**Available Options:**
- `-y, --non-interactive` - Skip all prompts, use defaults
- `--user-name <name>` - User name for configuration
- `--skill-level <level>` - beginner, intermediate, advanced
- `--output-folder <path>` - Output folder for BMAD artifacts
- `--modules <list>` - Comma-separated module list
- `--agents <list>` - Comma-separated agent list or 'all', 'none'
- `--workflows <list>` - Comma-separated workflow list or 'all', 'none'
- `--team <name>` - Install predefined team (fullstack, gamedev)
- `--profile <name>` - Installation profile (minimal, full, solo-dev, team)
**Modifiers:**
- `--agents=+dev` - Add agent to team/profile selection
- `--agents=-dev` - Remove agent from team/profile selection
**Available Teams:**
- `fullstack` - analyst, architect, pm, sm, ux-designer
- `gamedev` - game-designer, game-dev, game-architect, game-scrum-master
**Available Profiles:**
- `minimal` - Core + dev agent + essential workflows
- `full` - Everything (all modules, agents, workflows)
- `solo-dev` - Single developer setup
- `team` - Team collaboration setup
### 2. Initialize Your Project ### 2. Initialize Your Project
Load any agent in your IDE and run: Load any agent in your IDE and run:

View File

@ -0,0 +1,440 @@
# Non-Interactive Installation Guide
This guide helps you convert interactive BMAD installations to non-interactive CLI commands for automation, CI/CD pipelines, and scripted deployments.
## Table of Contents
- [Quick Start](#quick-start)
- [Migration from Interactive to CLI](#migration-from-interactive-to-cli)
- [Common Use Cases](#common-use-cases)
- [CLI Options Reference](#cli-options-reference)
- [Team-Based Installation](#team-based-installation)
- [Profile-Based Installation](#profile-based-installation)
- [Troubleshooting](#troubleshooting)
## Quick Start
### Minimal Non-Interactive Installation
```bash
npx bmad-method@alpha install -y
```
This installs BMAD with:
- Default user name from system (USER environment variable)
- Intermediate skill level
- Default output folder (`_bmad-output`)
- BMM module
- All agents and workflows from BMM
### Custom Non-Interactive Installation
```bash
npx bmad-method@alpha install -y \
--user-name=YourName \
--skill-level=advanced \
--output-folder=.artifacts
```
## Migration from Interactive to CLI
### Step 1: Note Your Current Configuration
If you have an existing BMAD installation, check your configuration:
```bash
# View your current configuration
cat _bmad/core/config.yaml
cat _bmad/bmm/config.yaml
```
Example output:
```yaml
user_name: Alice
user_skill_level: intermediate
output_folder: "{project-root}/_bmad-output"
communication_language: English
```
### Step 2: Convert to CLI Command
Based on your configuration, build the equivalent CLI command:
```bash
npx bmad-method@alpha install -y \
--user-name=Alice \
--skill-level=intermediate \
--output-folder=_bmad-output \
--communication-language=English
```
### Step 3: Replicate Module and Agent Selection
Check what agents and workflows you currently have:
```bash
# View installed agents
cat _bmad/_config/agents.csv
# View installed workflows
cat _bmad/_config/workflows.csv
```
If you have specific agents installed, add them to your command:
```bash
npx bmad-method@alpha install -y \
--user-name=Alice \
--agents=dev,architect,pm
```
## Common Use Cases
### 1. CI/CD Pipeline Installation
```yaml
# .github/workflows/setup-bmad.yml
name: Setup BMAD
on: [push]
jobs:
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install BMAD
run: npx bmad-method@alpha install -y --profile=minimal
```
### 2. Docker Container Setup
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
# Install BMAD non-interactively
RUN npx bmad-method@alpha install -y \
--user-name=ContainerUser \
--skill-level=intermediate \
--output-folder=.bmad-output
CMD ["npm", "start"]
```
### 3. Team Onboarding Script
```bash
#!/bin/bash
# onboard-developer.sh
echo "Setting up BMAD for $USER..."
npx bmad-method@alpha install -y \
--user-name=$USER \
--skill-level=intermediate \
--team=fullstack \
--output-folder=.bmad-output
echo "BMAD installation complete!"
```
### 4. Infrastructure as Code
```bash
# terraform/setup.sh
npx bmad-method@alpha install -y \
--user-name=TerraformBot \
--skill-level=advanced \
--modules=core,bmm \
--agents=dev,architect,analyst \
--workflows=create-prd,create-architecture,dev-story
```
### 5. Minimal Developer Setup
For developers who only need code generation:
```bash
npx bmad-method@alpha install -y \
--profile=minimal \
--user-name=$USER
```
## CLI Options Reference
### Core Options
| Option | Description | Default | Example |
|--------|-------------|---------|---------|
| `-y, --non-interactive` | Skip all prompts | `false` | `install -y` |
| `--user-name <name>` | User name | System user | `--user-name=Alice` |
| `--skill-level <level>` | Skill level | `intermediate` | `--skill-level=advanced` |
| `--output-folder <path>` | Output folder | `_bmad-output` | `--output-folder=.artifacts` |
| `--communication-language <lang>` | Communication language | `English` | `--communication-language=Spanish` |
| `--document-language <lang>` | Document language | `English` | `--document-language=French` |
### Module & Selection Options
| Option | Description | Example |
|--------|-------------|---------|
| `--modules <list>` | Comma-separated modules | `--modules=core,bmm,bmbb` |
| `--agents <list>` | Comma-separated agents | `--agents=dev,architect,pm` |
| `--workflows <list>` | Comma-separated workflows | `--workflows=create-prd,dev-story` |
### Team & Profile Options
| Option | Description | Example |
|--------|-------------|---------|
| `--team <name>` | Install predefined team | `--team=fullstack` |
| `--profile <name>` | Installation profile | `--profile=minimal` |
## Team-Based Installation
Teams are predefined bundles of agents and workflows optimized for specific use cases.
### Available Teams
#### Fullstack Team
```bash
npx bmad-method@alpha install -y --team=fullstack
```
**Includes:**
- Agents: analyst, architect, pm, sm, ux-designer
- Module: BMM
**Use for:** Full product development teams
#### Game Development Team
```bash
npx bmad-method@alpha install -y --team=gamedev
```
**Includes:**
- Agents: game-designer, game-dev, game-architect, game-scrum-master
- Workflows: brainstorm-game, game-brief, gdd, narrative
- Module: BMGD (Game Development)
**Use for:** Game development projects
### Modifying Team Selections
You can add or remove agents from a team:
```bash
# Add dev agent to fullstack team
npx bmad-method@alpha install -y --team=fullstack --agents=+dev
# Remove ux-designer from fullstack team
npx bmad-method@alpha install -y --team=fullstack --agents=-ux-designer
# Add and remove multiple
npx bmad-method@alpha install -y --team=fullstack --agents=+dev,+tea,-ux-designer
```
## Profile-Based Installation
Profiles are pre-configured installations for common scenarios.
### Available Profiles
#### Minimal Profile
```bash
npx bmad-method@alpha install -y --profile=minimal
```
**Includes:**
- Modules: core
- Agents: dev
- Workflows: create-tech-spec, quick-dev
**Use for:** Simple development, code generation only
#### Solo Developer Profile
```bash
npx bmad-method@alpha install -y --profile=solo-dev
```
**Includes:**
- Modules: core, bmm
- Agents: dev, architect, analyst, tech-writer
- Workflows: create-tech-spec, quick-dev, dev-story, code-review, create-prd, create-architecture
**Use for:** Individual developers working on full projects
#### Full Profile
```bash
npx bmad-method@alpha install -y --profile=full
```
**Includes:**
- All modules
- All agents
- All workflows
**Use for:** Maximum flexibility, exploring all BMAD features
#### Team Profile
```bash
npx bmad-method@alpha install -y --profile=team
```
**Includes:**
- Modules: core, bmm
- Agents: dev, architect, pm, sm, analyst, ux-designer
- Workflows: create-product-brief, create-prd, create-architecture, create-epics-and-stories, sprint-planning, create-story, dev-story, code-review, workflow-init
**Use for:** Team collaboration, full agile workflow
### Overriding Profile Settings
```bash
# Use minimal profile but add architect agent
npx bmad-method@alpha install -y --profile=minimal --agents=dev,architect
# Use solo-dev profile with custom output folder
npx bmad-method@alpha install -y --profile=solo-dev --output-folder=.custom
```
## Troubleshooting
### Issue: "Team not found"
**Solution:** Check available teams:
```bash
# List available teams in your installation
ls src/modules/*/teams/team-*.yaml
```
Available teams depend on installed modules. Ensure you have the required modules.
### Issue: "Agent not found in manifest"
**Solution:** The agent name might be incorrect. Check available agents:
```bash
# View all available agents
find src/modules -name "*.agent.yaml" -o -name "*-agent.md"
```
Common agent names: `dev`, `architect`, `pm`, `sm`, `analyst`, `ux-designer`, `tech-writer`
### Issue: "Installation hangs"
**Solution:** Ensure you're using the `-y` flag for non-interactive mode:
```bash
# Correct
npx bmad-method@alpha install -y
# Incorrect (will wait for input)
npx bmad-method@alpha install
```
### Issue: "Permission denied"
**Solution:** Check file permissions or run with appropriate privileges:
```bash
# Check current directory permissions
ls -la
# Ensure you have write permissions
chmod u+w .
```
### Issue: "Invalid skill level"
**Solution:** Use one of the valid skill levels:
- `beginner`
- `intermediate`
- `advanced`
```bash
# Correct
npx bmad-method@alpha install -y --skill-level=advanced
# Incorrect
npx bmad-method@alpha install -y --skill-level=expert
```
## Advanced Examples
### Reproducible Installation
Save your installation command for reproducibility:
```bash
#!/bin/bash
# install-bmad.sh - Reproducible BMAD installation
npx bmad-method@alpha install -y \
--user-name=ProjectBot \
--skill-level=intermediate \
--output-folder=_bmad-output \
--modules=core,bmm \
--agents=dev,architect,pm,analyst \
--workflows=create-prd,create-architecture,create-tech-spec,dev-story,code-review \
--communication-language=English \
--document-language=English
```
### Environment-Based Installation
Use environment variables for flexibility:
```bash
#!/bin/bash
# Detect user from environment
USER_NAME=${BMAD_USER:-$USER}
# Detect skill level from environment or default to intermediate
SKILL_LEVEL=${BMAD_SKILL_LEVEL:-intermediate}
npx bmad-method@alpha install -y \
--user-name=$USER_NAME \
--skill-level=$SKILL_LEVEL \
--output-folder=${BMAD_OUTPUT_FOLDER:-_bmad-output}
```
### Conditional Installation
```bash
#!/bin/bash
# Install different configurations based on environment
if [ "$CI" = "true" ]; then
# CI environment: minimal installation
npx bmad-method@alpha install -y --profile=minimal
elif [ "$TEAM_MODE" = "true" ]; then
# Team development: full team setup
npx bmad-method@alpha install -y --team=fullstack
else
# Local development: solo-dev profile
npx bmad-method@alpha install -y --profile=solo-dev --user-name=$USER
fi
```
## Next Steps
- Read the [main README](../README.md) for BMAD overview
- Explore [Custom Content Installation](./custom-content-installation.md)
- Join the [BMAD Discord](https://discord.gg/gk8jAdXWmj) community
## Feedback
Found an issue or have a suggestion? Please report it at:
https://github.com/bmad-code-org/BMAD-METHOD/issues

View File

@ -8,11 +8,44 @@ const ui = new UI();
module.exports = { module.exports = {
command: 'install', command: 'install',
description: 'Install BMAD Core agents and tools', description: `Install BMAD Core agents and tools
options: [],
Examples:
bmad install # Interactive installation
bmad install -y # Non-interactive with defaults
bmad install -y --user-name=Alice --skill-level=advanced
bmad install -y --team=fullstack # Install fullstack team
bmad install -y --team=fullstack --agents=+dev # Add dev to fullstack team
bmad install -y --agents=dev,architect,pm # Selective agents
bmad install -y --profile=minimal # Minimal profile
bmad install -y --workflows=create-prd,dev-story # Selective workflows
Special Values:
--agents=all, --agents=none, --agents=minimal
--workflows=all, --workflows=none, --workflows=minimal
Modifiers:
--agents=+dev Add agent to team/profile selection
--agents=-dev Remove agent from team/profile selection
Available Teams: fullstack, gamedev
Available Profiles: minimal, full, solo-dev, team`,
options: [
['-y, --non-interactive', 'Run without prompts, use defaults'],
['--user-name <name>', 'User name for configuration'],
['--skill-level <level>', 'User skill level (beginner, intermediate, advanced)'],
['--output-folder <path>', 'Output folder path for BMAD artifacts'],
['--modules <list>', 'Comma-separated list of modules to install (e.g., core,bmm)'],
['--agents <list>', 'Comma-separated list of agents to install (e.g., dev,architect,pm)'],
['--workflows <list>', 'Comma-separated list of workflows to install'],
['--team <name>', 'Install predefined team bundle (e.g., fullstack, gamedev)'],
['--profile <name>', 'Installation profile (minimal, full, solo-dev)'],
['--communication-language <lang>', 'Language for agent communication (default: English)'],
['--document-language <lang>', 'Language for generated documents (default: English)'],
],
action: async (options) => { action: async (options) => {
try { try {
const config = await ui.promptInstall(); const config = await ui.promptInstall(options);
// Handle cancel // Handle cancel
if (config.actionType === 'cancel') { if (config.actionType === 'cancel') {

View File

@ -5,6 +5,7 @@ const chalk = require('chalk');
const inquirer = require('inquirer'); const inquirer = require('inquirer');
const { getProjectRoot, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
const { CLIUtils } = require('../../../lib/cli-utils'); const { CLIUtils } = require('../../../lib/cli-utils');
const { getEnvironmentDefaults, resolveValue } = require('./env-resolver');
class ConfigCollector { class ConfigCollector {
constructor() { constructor() {
@ -792,6 +793,180 @@ class ConfigCollector {
} }
} }
/**
* Collect configuration for a module in non-interactive mode
* @param {string} moduleName - Module name
* @param {string} projectDir - Target project directory
* @param {Object} cliOptions - CLI options passed by user
* @returns {Object} Collected config for the module
*/
async collectModuleConfigNonInteractive(moduleName, projectDir, cliOptions = {}) {
this.currentProjectDir = projectDir;
// Load existing config if not already loaded
if (!this.existingConfig) {
await this.loadExistingConfig(projectDir);
}
// Initialize allAnswers if not already initialized
if (!this.allAnswers) {
this.allAnswers = {};
}
// Get environment defaults
const envDefaults = getEnvironmentDefaults();
// Try to load module config schema
let installerConfigPath = null;
let moduleConfigPath = null;
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
const customPath = this.customModulePaths.get(moduleName);
installerConfigPath = path.join(customPath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(customPath, 'module.yaml');
} else {
installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
}
// If not found, try to find via module manager
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
}
let configPath = null;
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else {
// No config for this module - use existing if available
if (this.existingConfig && this.existingConfig[moduleName]) {
this.collectedConfig[moduleName] = { ...this.existingConfig[moduleName] };
} else {
this.collectedConfig[moduleName] = {};
}
return this.collectedConfig[moduleName];
}
const configContent = await fs.readFile(configPath, 'utf8');
const moduleConfig = yaml.parse(configContent);
if (!moduleConfig) {
this.collectedConfig[moduleName] = {};
return this.collectedConfig[moduleName];
}
// Initialize module config
if (!this.collectedConfig[moduleName]) {
this.collectedConfig[moduleName] = {};
}
// Process each config item
const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
for (const key of configKeys) {
const item = moduleConfig[key];
// Skip if not a config object
if (!item || typeof item !== 'object') {
continue;
}
let value = null;
// Resolution order: CLI → ENV → existing → default → hardcoded
if (moduleName === 'core') {
// Core module has special mappings
if (key === 'user_name') {
value = resolveValue(cliOptions.userName, null, envDefaults.userName);
} else if (key === 'user_skill_level') {
value = resolveValue(cliOptions.skillLevel, null, item.default || 'intermediate');
} else if (key === 'communication_language') {
value = resolveValue(
cliOptions.communicationLanguage,
null,
envDefaults.communicationLanguage,
);
} else if (key === 'document_output_language') {
value = resolveValue(cliOptions.documentLanguage, null, envDefaults.documentLanguage);
} else if (key === 'output_folder') {
value = resolveValue(cliOptions.outputFolder, null, item.default);
} else if (item.default !== undefined) {
value = item.default;
}
} else {
// For other modules, use defaults
if (item.default !== undefined) {
value = item.default;
} else if (this.existingConfig && this.existingConfig[moduleName]) {
value = this.existingConfig[moduleName][key];
}
}
// Process result template if present
let result;
if (item.result && typeof item.result === 'string') {
result = item.result;
if (typeof value === 'string') {
result = result.replace('{value}', value);
} else if (value !== undefined && value !== null) {
result = result.replace('{value}', String(value));
}
// Replace references to other config values
result = result.replaceAll(/{([^}]+)}/g, (match, configKey) => {
if (configKey === 'project-root' || configKey === 'value') {
return match;
}
// Look in collected config
let configValue = this.collectedConfig[moduleName]?.[configKey];
if (!configValue) {
for (const mod of Object.keys(this.collectedConfig)) {
if (mod !== '_meta' && this.collectedConfig[mod]?.[configKey]) {
configValue = this.collectedConfig[mod][configKey];
if (typeof configValue === 'string' && configValue.includes('{project-root}/')) {
configValue = configValue.replace('{project-root}/', '');
}
break;
}
}
}
return configValue || match;
});
} else if (item.result) {
result = value;
} else {
result = value;
}
// Store the result
this.collectedConfig[moduleName][key] = result;
this.allAnswers[`${moduleName}_${key}`] = result;
}
// Copy existing values for fields not in schema
if (this.existingConfig && this.existingConfig[moduleName]) {
for (const [key, value] of Object.entries(this.existingConfig[moduleName])) {
if (this.collectedConfig[moduleName][key] === undefined) {
this.collectedConfig[moduleName][key] = value;
this.allAnswers[`${moduleName}_${key}`] = value;
}
}
}
return this.collectedConfig[moduleName];
}
/** /**
* Replace placeholders in a string with collected config values * Replace placeholders in a string with collected config values
* @param {string} str - String with placeholders * @param {string} str - String with placeholders

View File

@ -0,0 +1,120 @@
const os = require('node:os');
/**
* Environment Variable Resolver
*
* Resolves configuration values from environment variables
* with fallbacks to system defaults.
*/
/**
* Get user name from environment variables
* Tries USER, USERNAME, LOGNAME in order, falls back to system username or 'User'
* @returns {string} User name
*/
function getUserName() {
// Try common environment variables
const envUser = process.env.USER || process.env.USERNAME || process.env.LOGNAME;
if (envUser) {
return envUser;
}
// Try Node.js os.userInfo()
try {
const userInfo = os.userInfo();
if (userInfo.username) {
return userInfo.username;
}
} catch (error) {
// os.userInfo() can fail in some environments
}
// Final fallback
return 'User';
}
/**
* Get system language from environment variables
* Tries LANG, LC_ALL, falls back to 'English'
* @returns {string} Language name
*/
function getSystemLanguage() {
const lang = process.env.LANG || process.env.LC_ALL;
if (!lang) {
return 'English';
}
// Parse language from locale string (e.g., 'en_US.UTF-8' -> 'English')
const langCode = lang.split('_')[0].toLowerCase();
// Map common language codes to full names
const languageMap = {
en: 'English',
es: 'Spanish',
fr: 'French',
de: 'German',
it: 'Italian',
pt: 'Portuguese',
ru: 'Russian',
ja: 'Japanese',
zh: 'Chinese',
ko: 'Korean',
ar: 'Arabic',
hi: 'Hindi',
};
return languageMap[langCode] || 'English';
}
/**
* Get home directory from environment
* @returns {string} Home directory path
*/
function getHomeDirectory() {
return process.env.HOME || process.env.USERPROFILE || os.homedir();
}
/**
* Resolve a config value with priority: CLI > ENV > default
* @param {*} cliValue - Value from CLI argument
* @param {string} envVar - Environment variable name to check
* @param {*} defaultValue - Default value if neither CLI nor ENV is set
* @returns {*} Resolved value
*/
function resolveValue(cliValue, envVar, defaultValue) {
// CLI value has highest priority
if (cliValue !== undefined && cliValue !== null) {
return cliValue;
}
// Try environment variable
if (envVar && process.env[envVar]) {
return process.env[envVar];
}
// Use default
return defaultValue;
}
/**
* Get all environment-based defaults
* @returns {Object} Default config values from environment
*/
function getEnvironmentDefaults() {
return {
userName: getUserName(),
communicationLanguage: getSystemLanguage(),
documentLanguage: getSystemLanguage(),
homeDirectory: getHomeDirectory(),
};
}
module.exports = {
getUserName,
getSystemLanguage,
getHomeDirectory,
resolveValue,
getEnvironmentDefaults,
};

View File

@ -1041,6 +1041,9 @@ class Installer {
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], { const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
ides: config.ides || [], ides: config.ides || [],
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
selectedAgents: config.cliOptions?.agents || null,
selectedWorkflows: config.cliOptions?.workflows || null,
installMode: config.cliOptions ? 'non-interactive' : 'interactive',
}); });
// Custom modules are now included in the main modules list - no separate tracking needed // Custom modules are now included in the main modules list - no separate tracking needed

View File

@ -65,11 +65,20 @@ class ManifestGenerator {
// Filter out any undefined/null values from IDE list // Filter out any undefined/null values from IDE list
this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string'); this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string');
// Get filtering options from options
const selectedAgents = options.selectedAgents || null;
const selectedWorkflows = options.selectedWorkflows || null;
// Store installation mode and options for manifest
this.installMode = options.installMode || 'interactive';
this.selectedAgentsList = selectedAgents;
this.selectedWorkflowsList = selectedWorkflows;
// Collect workflow data // Collect workflow data
await this.collectWorkflows(selectedModules); await this.collectWorkflows(selectedModules, selectedWorkflows);
// Collect agent data - use updatedModules which includes all installed modules // Collect agent data - use updatedModules which includes all installed modules
await this.collectAgents(this.updatedModules); await this.collectAgents(this.updatedModules, selectedAgents);
// Collect task data // Collect task data
await this.collectTasks(this.updatedModules); await this.collectTasks(this.updatedModules);
@ -100,8 +109,10 @@ class ManifestGenerator {
/** /**
* Collect all workflows from core and selected modules * Collect all workflows from core and selected modules
* Scans the INSTALLED bmad directory, not the source * Scans the INSTALLED bmad directory, not the source
* @param {Array} selectedModules - Modules to scan for workflows
* @param {Array|null} selectedWorkflows - Optional array of workflow names to filter by
*/ */
async collectWorkflows(selectedModules) { async collectWorkflows(selectedModules, selectedWorkflows = null) {
this.workflows = []; this.workflows = [];
// Use updatedModules which already includes deduplicated 'core' + selectedModules // Use updatedModules which already includes deduplicated 'core' + selectedModules
@ -113,6 +124,50 @@ class ManifestGenerator {
this.workflows.push(...moduleWorkflows); this.workflows.push(...moduleWorkflows);
} }
} }
// Apply filtering if selectedWorkflows is provided
if (selectedWorkflows && Array.isArray(selectedWorkflows) && selectedWorkflows.length > 0) {
this.workflows = this.filterWorkflows(this.workflows, selectedWorkflows);
}
}
/**
* Filter workflows by name matching (supports wildcards)
* @param {Array} workflows - Array of workflow objects
* @param {Array} selectedWorkflows - Array of workflow names to filter by (supports * wildcard)
* @returns {Array} Filtered workflows
*/
filterWorkflows(workflows, selectedWorkflows) {
if (!selectedWorkflows || selectedWorkflows.length === 0) {
return workflows;
}
// Check for special values
if (selectedWorkflows.includes('all')) {
return workflows;
}
if (selectedWorkflows.includes('none')) {
return [];
}
return workflows.filter((workflow) => {
const workflowName = workflow.name;
return selectedWorkflows.some((pattern) => {
// Exact match
if (pattern === workflowName) {
return true;
}
// Wildcard matching: create-* matches create-prd, create-tech-spec, etc.
if (pattern.includes('*')) {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
return regex.test(workflowName);
}
return false;
});
});
} }
/** /**
@ -197,8 +252,10 @@ class ManifestGenerator {
/** /**
* Collect all agents from core and selected modules * Collect all agents from core and selected modules
* Scans the INSTALLED bmad directory, not the source * Scans the INSTALLED bmad directory, not the source
* @param {Array} selectedModules - Modules to scan for agents
* @param {Array|null} selectedAgents - Optional array of agent names to filter by
*/ */
async collectAgents(selectedModules) { async collectAgents(selectedModules, selectedAgents = null) {
this.agents = []; this.agents = [];
// Use updatedModules which already includes deduplicated 'core' + selectedModules // Use updatedModules which already includes deduplicated 'core' + selectedModules
@ -224,6 +281,50 @@ class ManifestGenerator {
this.agents.push(...standaloneAgents); this.agents.push(...standaloneAgents);
} }
} }
// Apply filtering if selectedAgents is provided
if (selectedAgents && Array.isArray(selectedAgents) && selectedAgents.length > 0) {
this.agents = this.filterAgents(this.agents, selectedAgents);
}
}
/**
* Filter agents by name matching (supports wildcards)
* @param {Array} agents - Array of agent objects
* @param {Array} selectedAgents - Array of agent names to filter by (supports * wildcard)
* @returns {Array} Filtered agents
*/
filterAgents(agents, selectedAgents) {
if (!selectedAgents || selectedAgents.length === 0) {
return agents;
}
// Check for special values
if (selectedAgents.includes('all')) {
return agents;
}
if (selectedAgents.includes('none')) {
return [];
}
return agents.filter((agent) => {
const agentName = agent.name;
return selectedAgents.some((pattern) => {
// Exact match
if (pattern === agentName) {
return true;
}
// Wildcard matching: dev* matches dev, dev-story, etc.
if (pattern.includes('*')) {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
return regex.test(agentName);
}
return false;
});
});
} }
/** /**
@ -462,11 +563,20 @@ class ManifestGenerator {
version: packageJson.version, version: packageJson.version,
installDate: new Date().toISOString(), installDate: new Date().toISOString(),
lastUpdated: new Date().toISOString(), lastUpdated: new Date().toISOString(),
installMode: this.installMode || 'interactive',
}, },
modules: this.modules, // Include ALL modules (standard and custom) modules: this.modules, // Include ALL modules (standard and custom)
ides: this.selectedIdes, ides: this.selectedIdes,
}; };
// Add selective installation info if filters were applied
if (this.selectedAgentsList && this.selectedAgentsList.length > 0) {
manifest.selectedAgents = this.selectedAgentsList;
}
if (this.selectedWorkflowsList && this.selectedWorkflowsList.length > 0) {
manifest.selectedWorkflows = this.selectedWorkflowsList;
}
// Clean the manifest to remove any non-serializable values // Clean the manifest to remove any non-serializable values
const cleanManifest = structuredClone(manifest); const cleanManifest = structuredClone(manifest);

View File

@ -0,0 +1,198 @@
const { getProfile } = require('../profiles/definitions');
/**
* CLI Options Parser
*
* Parses and normalizes CLI options for non-interactive installation.
* Handles profiles, comma-separated lists, special values, and validation.
*/
/**
* Parse comma-separated list into array
* @param {string|undefined} value - Comma-separated string or undefined
* @returns {string[]|null} Array of trimmed values or null if undefined
*/
function parseList(value) {
if (!value) {
return null;
}
if (typeof value !== 'string') {
return null;
}
return value
.split(',')
.map((item) => item.trim())
.filter((item) => item.length > 0);
}
/**
* Check if value is a special keyword
* @param {string|string[]|null} value - Value to check
* @returns {boolean} True if special keyword (all, none, minimal)
*/
function isSpecialValue(value) {
if (Array.isArray(value) && value.length === 1) {
value = value[0];
}
return value === 'all' || value === 'none' || value === 'minimal';
}
/**
* Separate additive (+) and subtractive (-) modifiers from list
* @param {string[]} list - Array of items, some may have +/- prefix
* @returns {Object} { base: [], add: [], remove: [] }
*/
function separateModifiers(list) {
const result = {
base: [],
add: [],
remove: [],
};
if (!list || !Array.isArray(list)) {
return result;
}
for (const item of list) {
if (item.startsWith('+')) {
result.add.push(item.substring(1));
} else if (item.startsWith('-')) {
result.remove.push(item.substring(1));
} else {
result.base.push(item);
}
}
return result;
}
/**
* Apply modifiers to a base list (additive/subtractive)
* @param {string[]} baseList - Base list of items
* @param {string[]} add - Items to add
* @param {string[]} remove - Items to remove
* @returns {string[]} Modified list
*/
function applyModifiers(baseList, add = [], remove = []) {
let result = [...baseList];
// Add items
for (const item of add) {
if (!result.includes(item)) {
result.push(item);
}
}
// Remove items
for (const item of remove) {
result = result.filter((i) => i !== item);
}
return result;
}
/**
* Parse and normalize CLI options
* @param {Object} cliOptions - Raw CLI options from commander
* @returns {Object} Normalized options
*/
function parseOptions(cliOptions) {
const normalized = {
nonInteractive: cliOptions.nonInteractive || false,
userName: cliOptions.userName,
skillLevel: cliOptions.skillLevel,
outputFolder: cliOptions.outputFolder,
communicationLanguage: cliOptions.communicationLanguage,
documentLanguage: cliOptions.documentLanguage,
modules: parseList(cliOptions.modules),
agents: parseList(cliOptions.agents),
workflows: parseList(cliOptions.workflows),
team: cliOptions.team,
profile: cliOptions.profile,
};
// Expand profile if provided
if (normalized.profile) {
const profile = getProfile(normalized.profile);
if (!profile) {
throw new Error(
`Unknown profile: ${normalized.profile}. Valid profiles: minimal, full, solo-dev, team`,
);
}
// Profile provides defaults, but CLI options override
normalized.profileModules = profile.modules;
normalized.profileAgents = profile.agents;
normalized.profileWorkflows = profile.workflows;
// If no explicit modules/agents/workflows, use profile values
if (!normalized.modules) {
normalized.modules = Array.isArray(profile.modules) ? profile.modules : profile.modules;
}
if (!normalized.agents) {
normalized.agents = Array.isArray(profile.agents) ? profile.agents : profile.agents;
}
if (!normalized.workflows) {
normalized.workflows = Array.isArray(profile.workflows)
? profile.workflows
: profile.workflows;
}
}
return normalized;
}
/**
* Validate parsed options for conflicts and errors
* @param {Object} options - Parsed options
* @returns {Object} { valid: boolean, errors: string[] }
*/
function validateOptions(options) {
const errors = [];
// Validate skill level
if (options.skillLevel) {
const validLevels = ['beginner', 'intermediate', 'advanced'];
if (!validLevels.includes(options.skillLevel.toLowerCase())) {
errors.push(
`Invalid skill level: ${options.skillLevel}. Valid values: beginner, intermediate, advanced`,
);
}
}
// Validate profile
if (options.profile) {
const validProfiles = ['minimal', 'full', 'solo-dev', 'team'];
if (!validProfiles.includes(options.profile.toLowerCase())) {
errors.push(
`Invalid profile: ${options.profile}. Valid values: minimal, full, solo-dev, team`,
);
}
}
// Check for empty selections
if (options.agents && Array.isArray(options.agents) && options.agents.length === 0) {
errors.push('Agents list cannot be empty');
}
if (options.workflows && Array.isArray(options.workflows) && options.workflows.length === 0) {
errors.push('Workflows list cannot be empty');
}
return {
valid: errors.length === 0,
errors,
};
}
module.exports = {
parseList,
isSpecialValue,
separateModifiers,
applyModifiers,
parseOptions,
validateOptions,
};

View File

@ -0,0 +1,103 @@
/**
* Installation Profile Definitions
*
* Profiles are pre-defined combinations of modules, agents, and workflows
* for common use cases. Users can select a profile with --profile=<name>
* and override specific selections with CLI flags.
*/
const PROFILES = {
minimal: {
name: 'minimal',
description: 'Minimal installation - core + dev agent + essential workflows',
modules: ['core'],
agents: ['dev'],
workflows: ['create-tech-spec', 'quick-dev'],
},
full: {
name: 'full',
description: 'Full installation - all modules, agents, and workflows',
modules: 'all',
agents: 'all',
workflows: 'all',
},
'solo-dev': {
name: 'solo-dev',
description: 'Single developer setup - dev tools and planning workflows',
modules: ['core', 'bmm'],
agents: ['dev', 'architect', 'analyst', 'tech-writer'],
workflows: [
'create-tech-spec',
'quick-dev',
'dev-story',
'code-review',
'create-prd',
'create-architecture',
],
},
team: {
name: 'team',
description: 'Team collaboration setup - planning and execution workflows',
modules: ['core', 'bmm'],
agents: ['dev', 'architect', 'pm', 'sm', 'analyst', 'ux-designer'],
workflows: [
'create-product-brief',
'create-prd',
'create-architecture',
'create-epics-and-stories',
'sprint-planning',
'create-story',
'dev-story',
'code-review',
'workflow-init',
],
},
};
/**
* Get a profile by name
* @param {string} name - Profile name (minimal, full, solo-dev, team)
* @returns {Object|null} Profile definition or null if not found
*/
function getProfile(name) {
if (!name) {
return null;
}
const profile = PROFILES[name.toLowerCase()];
if (!profile) {
return null;
}
// Return a copy to prevent mutation
return { ...profile };
}
/**
* Get all available profile names
* @returns {string[]} Array of profile names
*/
function getProfileNames() {
return Object.keys(PROFILES);
}
/**
* Get profile descriptions for help text
* @returns {Object} Map of profile name to description
*/
function getProfileDescriptions() {
const descriptions = {};
for (const [name, profile] of Object.entries(PROFILES)) {
descriptions[name] = profile.description;
}
return descriptions;
}
module.exports = {
getProfile,
getProfileNames,
getProfileDescriptions,
};

View File

@ -0,0 +1,181 @@
const fs = require('node:fs');
const path = require('node:path');
const yaml = require('yaml');
const { glob } = require('glob');
/**
* Team Loader
*
* Discovers and loads team bundles from module definitions.
* Teams are predefined collections of agents and workflows for common use cases.
*/
/**
* Discover all available teams across modules
* @param {string} projectRoot - Project root directory
* @returns {Promise<Object[]>} Array of team metadata { name, module, path, description }
*/
async function discoverTeams(projectRoot) {
const teams = [];
const pattern = path.join(projectRoot, 'src/modules/*/teams/team-*.yaml');
try {
const files = await glob(pattern, { absolute: true });
for (const filePath of files) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const teamData = yaml.parse(content);
// Extract team name from filename (team-fullstack.yaml -> fullstack)
const filename = path.basename(filePath);
const teamName = filename.replace(/^team-/, '').replace(/\.yaml$/, '');
// Extract module name from path
const moduleName = path.basename(path.dirname(path.dirname(filePath)));
teams.push({
name: teamName,
module: moduleName,
path: filePath,
description: teamData.bundle?.description || 'No description',
bundleName: teamData.bundle?.name || teamName,
icon: teamData.bundle?.icon || '👥',
});
} catch (error) {
// Skip files that can't be parsed
console.warn(`Warning: Could not parse team file ${filePath}: ${error.message}`);
}
}
return teams;
} catch (error) {
// If glob fails, return empty array
return [];
}
}
/**
* Load a specific team by name
* @param {string} teamName - Team name (e.g., 'fullstack', 'gamedev')
* @param {string} projectRoot - Project root directory
* @returns {Promise<Object>} Team data with metadata
*/
async function loadTeam(teamName, projectRoot) {
if (!teamName) {
throw new Error('Team name is required');
}
// Discover all teams
const teams = await discoverTeams(projectRoot);
// Find matching team
const team = teams.find((t) => t.name.toLowerCase() === teamName.toLowerCase());
if (!team) {
// Provide helpful error with suggestions
const availableTeams = teams.map((t) => t.name).join(', ');
throw new Error(
`Team '${teamName}' not found. Available teams: ${availableTeams || 'none'}`,
);
}
// Load full team definition
const content = fs.readFileSync(team.path, 'utf8');
const teamData = yaml.parse(content);
return {
name: team.name,
module: team.module,
description: team.description,
bundleName: team.bundleName,
icon: team.icon,
agents: teamData.agents || [],
workflows: teamData.workflows || [],
party: teamData.party,
};
}
/**
* Expand team definition to full agents and workflows list
* @param {string} teamName - Team name
* @param {string} projectRoot - Project root directory
* @returns {Promise<Object>} { agents: [], workflows: [], module: string }
*/
async function expandTeam(teamName, projectRoot) {
const team = await loadTeam(teamName, projectRoot);
return {
agents: team.agents || [],
workflows: team.workflows || [],
module: team.module,
description: team.description,
};
}
/**
* Apply modifiers to team selections (additive/subtractive)
* @param {Object} team - Team expansion result
* @param {string[]} agentModifiers - Agent modifiers (+agent, -agent)
* @param {string[]} workflowModifiers - Workflow modifiers (+workflow, -workflow)
* @returns {Object} Modified team with updated agents/workflows
*/
function applyTeamModifiers(team, agentModifiers = [], workflowModifiers = []) {
const result = {
...team,
agents: [...team.agents],
workflows: [...team.workflows],
};
// Parse and apply agent modifiers
for (const modifier of agentModifiers) {
if (modifier.startsWith('+')) {
const agent = modifier.substring(1);
if (!result.agents.includes(agent)) {
result.agents.push(agent);
}
} else if (modifier.startsWith('-')) {
const agent = modifier.substring(1);
result.agents = result.agents.filter((a) => a !== agent);
}
}
// Parse and apply workflow modifiers
for (const modifier of workflowModifiers) {
if (modifier.startsWith('+')) {
const workflow = modifier.substring(1);
if (!result.workflows.includes(workflow)) {
result.workflows.push(workflow);
}
} else if (modifier.startsWith('-')) {
const workflow = modifier.substring(1);
result.workflows = result.workflows.filter((w) => w !== workflow);
}
}
return result;
}
/**
* Get team descriptions for help text
* @param {string} projectRoot - Project root directory
* @returns {Promise<Object>} Map of team name to description
*/
async function getTeamDescriptions(projectRoot) {
const teams = await discoverTeams(projectRoot);
const descriptions = {};
for (const team of teams) {
descriptions[team.name] = team.description;
}
return descriptions;
}
module.exports = {
discoverTeams,
loadTeam,
expandTeam,
applyTeamModifiers,
getTeamDescriptions,
};

View File

@ -12,9 +12,15 @@ const { CustomHandler } = require('../installers/lib/custom/handler');
class UI { class UI {
/** /**
* Prompt for installation configuration * Prompt for installation configuration
* @param {Object} cliOptions - CLI options for non-interactive mode
* @returns {Object} Installation configuration * @returns {Object} Installation configuration
*/ */
async promptInstall() { async promptInstall(cliOptions = {}) {
// Handle non-interactive mode
if (cliOptions.nonInteractive) {
return await this.buildNonInteractiveConfig(cliOptions);
}
CLIUtils.displayLogo(); CLIUtils.displayLogo();
// Display changelog link // Display changelog link
@ -1457,6 +1463,139 @@ class UI {
return result; return result;
} }
/**
* Build non-interactive installation configuration
* @param {Object} cliOptions - CLI options
* @returns {Object} Installation configuration
*/
async buildNonInteractiveConfig(cliOptions) {
const { parseOptions } = require('../installers/lib/core/options-parser');
const { expandTeam, applyTeamModifiers } = require('../installers/lib/teams/team-loader');
const { getProjectRoot } = require('./project-root');
const { getEnvironmentDefaults } = require('../installers/lib/core/env-resolver');
console.log(chalk.cyan('🤖 Running non-interactive installation...\n'));
// Parse and normalize options
const options = parseOptions(cliOptions);
const envDefaults = getEnvironmentDefaults();
// Determine directory
const directory = process.cwd();
// Check for existing installation
const { Installer } = require('../installers/lib/core/installer');
const installer = new Installer();
const { bmadDir, hasExistingInstall } = await installer.findBmadDir(directory);
const actionType = hasExistingInstall ? 'update' : 'install';
console.log(chalk.dim(` Directory: ${directory}`));
console.log(chalk.dim(` Action: ${actionType === 'install' ? 'New installation' : 'Update existing installation'}\n`));
// Determine modules to install
let selectedModules = [];
if (options.team) {
// Team-based installation
console.log(chalk.cyan(`📦 Loading team: ${options.team}...`));
try {
const projectRoot = getProjectRoot();
let teamExpansion = await expandTeam(options.team, projectRoot);
// Apply modifiers if present
if (options.agents || options.workflows) {
const { separateModifiers } = require('../installers/lib/core/options-parser');
const agentMods = options.agents ? separateModifiers(options.agents) : { base: [], add: [], remove: [] };
const workflowMods = options.workflows ? separateModifiers(options.workflows) : { base: [], add: [], remove: [] };
// If base is provided, replace team selections completely
if (agentMods.base.length > 0) {
teamExpansion.agents = agentMods.base;
}
if (workflowMods.base.length > 0) {
teamExpansion.workflows = workflowMods.base;
}
// Apply modifiers
teamExpansion = applyTeamModifiers(
teamExpansion,
[...agentMods.add.map((a) => `+${a}`), ...agentMods.remove.map((a) => `-${a}`)],
[...workflowMods.add.map((w) => `+${w}`), ...workflowMods.remove.map((w) => `-${w}`)],
);
}
options.agents = teamExpansion.agents;
options.workflows = teamExpansion.workflows;
// Determine module from team
if (teamExpansion.module && !selectedModules.includes(teamExpansion.module)) {
selectedModules.push(teamExpansion.module);
}
console.log(chalk.green(` ✓ Team loaded: ${options.team}`));
console.log(chalk.dim(` Agents: ${teamExpansion.agents.join(', ')}`));
if (teamExpansion.workflows && teamExpansion.workflows.length > 0) {
console.log(chalk.dim(` Workflows: ${teamExpansion.workflows.join(', ')}`));
}
console.log('');
} catch (error) {
console.error(chalk.red(` ✗ Failed to load team: ${error.message}`));
process.exit(1);
}
} else if (options.modules) {
// Module-based installation
if (options.modules === 'all') {
selectedModules = ['bmm', 'bmbb', 'cis', 'bmgd'];
} else if (Array.isArray(options.modules)) {
selectedModules = options.modules.filter((m) => m !== 'core');
}
} else if (options.profile) {
// Profile-based installation
const { getProfile } = require('../installers/lib/profiles/definitions');
const profile = getProfile(options.profile);
if (profile.modules === 'all') {
selectedModules = ['bmm', 'bmbb', 'cis', 'bmgd'];
} else if (Array.isArray(profile.modules)) {
selectedModules = profile.modules.filter((m) => m !== 'core');
}
} else {
// Default: install bmm
selectedModules = ['bmm'];
}
console.log(chalk.cyan(`📦 Modules: ${selectedModules.length > 0 ? selectedModules.join(', ') : 'core only'}\n`));
// Build core configuration
const coreConfig = {
user_name: options.userName || envDefaults.userName,
user_skill_level: options.skillLevel || 'intermediate',
communication_language: options.communicationLanguage || envDefaults.communicationLanguage,
document_output_language: options.documentLanguage || envDefaults.documentLanguage,
output_folder: options.outputFolder || '_bmad-output',
};
console.log(chalk.cyan('⚙️ Configuration:'));
console.log(chalk.dim(` User: ${coreConfig.user_name}`));
console.log(chalk.dim(` Skill Level: ${coreConfig.user_skill_level}`));
console.log(chalk.dim(` Language: ${coreConfig.communication_language}\n`));
// Return installation configuration
return {
actionType,
directory,
installCore: true,
modules: selectedModules,
ides: ['claude-code'], // Default to Claude Code for non-interactive
skipIde: false,
coreConfig,
customContent: { hasCustomContent: false },
enableAgentVibes: false,
agentVibesInstalled: false,
// Pass through CLI options for downstream use
cliOptions: options,
};
}
} }
module.exports = { UI }; module.exports = { UI };

View File

@ -0,0 +1,178 @@
#!/bin/bash
# Test script for non-interactive BMAD installation
# Tests various CLI options and validates installation
set -e # Exit on error
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
TEST_DIR="/tmp/bmad-test-$(date +%s)"
echo "🧪 BMAD Non-Interactive Installation Test Suite"
echo "================================================"
echo "Test directory: $TEST_DIR"
echo ""
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Track test results
TESTS_PASSED=0
TESTS_FAILED=0
# Helper function to run a test
run_test() {
local test_name="$1"
local test_dir="$TEST_DIR/$test_name"
shift
echo -e "${YELLOW}▶ Running: $test_name${NC}"
mkdir -p "$test_dir"
cd "$test_dir"
if "$@"; then
echo -e "${GREEN}✓ PASSED: $test_name${NC}"
TESTS_PASSED=$((TESTS_PASSED + 1))
return 0
else
echo -e "${RED}✗ FAILED: $test_name${NC}"
TESTS_FAILED=$((TESTS_FAILED + 1))
return 1
fi
}
# Helper to verify installation
verify_installation() {
local dir="$1"
local expected_agents="$2" # comma-separated list
local expected_workflows="$3" # comma-separated list
# Check _bmad directory exists
if [ ! -d "$dir/_bmad" ]; then
echo "❌ _bmad directory not found"
return 1
fi
# Check manifest exists
if [ ! -f "$dir/_bmad/_config/manifest.yaml" ]; then
echo "❌ manifest.yaml not found"
return 1
fi
# Check agents CSV if expected agents provided
if [ -n "$expected_agents" ]; then
if [ ! -f "$dir/_bmad/_config/agents.csv" ]; then
echo "❌ agents.csv not found"
return 1
fi
IFS=',' read -ra AGENTS <<< "$expected_agents"
for agent in "${AGENTS[@]}"; do
if ! grep -q "$agent" "$dir/_bmad/_config/agents.csv"; then
echo "❌ Agent '$agent' not found in agents.csv"
return 1
fi
done
fi
# Check workflows CSV if expected workflows provided
if [ -n "$expected_workflows" ]; then
if [ ! -f "$dir/_bmad/_config/workflows.csv" ]; then
echo "❌ workflows.csv not found"
return 1
fi
IFS=',' read -ra WORKFLOWS <<< "$expected_workflows"
for workflow in "${WORKFLOWS[@]}"; do
if ! grep -q "$workflow" "$dir/_bmad/_config/workflows.csv"; then
echo "❌ Workflow '$workflow' not found in workflows.csv"
return 1
fi
done
fi
echo "✓ Installation verified"
return 0
}
# Test 1: Minimal non-interactive installation
run_test "test-01-minimal-install" bash -c "
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y
verify_installation . '' ''
"
# Test 2: Non-interactive with custom user name
run_test "test-02-custom-user" bash -c "
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --user-name=TestUser
verify_installation . '' ''
grep -q 'user_name: TestUser' _bmad/core/config.yaml
"
# Test 3: Selective agent installation
run_test "test-03-selective-agents" bash -c "
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --agents=dev,architect
verify_installation . 'dev,architect' ''
"
# Test 4: Selective workflow installation
run_test "test-04-selective-workflows" bash -c "
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --workflows=create-prd,create-tech-spec
verify_installation . '' 'create-prd,create-tech-spec'
"
# Test 5: Team-based installation (fullstack)
run_test "test-05-team-fullstack" bash -c "
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --team=fullstack
verify_installation . 'analyst,architect,pm,sm,ux-designer' ''
"
# Test 6: Profile-based installation (minimal)
run_test "test-06-profile-minimal" bash -c "
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --profile=minimal
verify_installation . 'dev' 'create-tech-spec,quick-dev'
"
# Test 7: Multiple CLI options
run_test "test-07-multiple-options" bash -c "
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y \
--user-name=FullTest \
--skill-level=advanced \
--output-folder=.output \
--agents=dev,architect
verify_installation . 'dev,architect' ''
grep -q 'user_name: FullTest' _bmad/core/config.yaml
grep -q 'user_skill_level: advanced' _bmad/core/config.yaml
"
# Test 8: Manifest tracking
run_test "test-08-manifest-tracking" bash -c "
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --agents=dev
verify_installation . 'dev' ''
grep -q 'installMode: non-interactive' _bmad/_config/manifest.yaml
grep -q 'selectedAgents:' _bmad/_config/manifest.yaml
"
# Cleanup
echo ""
echo "🧹 Cleaning up test directory: $TEST_DIR"
rm -rf "$TEST_DIR"
# Summary
echo ""
echo "================================================"
echo "Test Summary"
echo "================================================"
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
echo ""
if [ $TESTS_FAILED -eq 0 ]; then
echo -e "${GREEN}✓ All tests passed!${NC}"
exit 0
else
echo -e "${RED}✗ Some tests failed${NC}"
exit 1
fi