This commit is contained in:
Vladimir Hrusovsky 2026-02-04 00:36:57 +01:00 committed by GitHub
commit a934157b49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 622 additions and 52 deletions

View File

@ -30,6 +30,14 @@ npx bmad-method install
Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, Windsurf, etc.) in the project folder.
**Non-Interactive Installation**: For CI/CD pipelines or automated deployments, use command-line flags:
```bash
npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes
```
See [Non-Interactive Installation Guide](docs/non-interactive-installation.md) for all available options.
> **Not sure what to do?** Run `/bmad-help` — it tells you exactly what's next and what's optional. You can also ask it questions like:
- `/bmad-help How should I build a web app for my TShirt Business that can scale to millions?`

View File

@ -0,0 +1,314 @@
---
title: Non-Interactive Installation
description: Install BMAD using command-line flags for CI/CD pipelines and automated deployments
---
# Non-Interactive Installation
BMAD now supports non-interactive installation through command-line flags. This is particularly useful for:
- Automated deployments and CI/CD pipelines
- Scripted installations
- Batch installations across multiple projects
- Quick installations with known configurations
## Installation Modes
### 1. Fully Interactive (Default)
Run without any flags to use the traditional interactive prompts:
```bash
npx bmad-method install
```
### 2. Fully Non-Interactive
Provide all required flags to skip all prompts:
```bash
npx bmad-method install \
--directory /path/to/project \
--modules bmm,bmb \
--tools claude-code,cursor \
--user-name "John Doe" \
--communication-language English \
--document-output-language English \
--output-folder _bmad-output
```
### 3. Semi-Interactive (Graceful Fallback)
Provide some flags and let BMAD prompt for the rest:
```bash
npx bmad-method install \
--directory /path/to/project \
--modules bmm
```
In this case, BMAD will:
- Use the provided directory and modules
- Prompt for tool selection
- Prompt for core configuration
### 4. Quick Install with Defaults
Use the `-y` or `--yes` flag to accept all defaults:
```bash
npx bmad-method install --yes
```
This will:
- Install to the current directory
- Skip custom content prompts
- Use default values for all configuration
- Use previously configured tools (or skip tool configuration if none exist)
### 5. Install Without Tools
To skip tool/IDE configuration entirely:
**Option 1: Use --tools none**
```bash
npx bmad-method install --directory ~/myapp --modules bmm --tools none
```
**Option 2: Use --yes flag (if no tools were previously configured)**
```bash
npx bmad-method install --yes
```
**Option 3: Omit --tools and select "None" in the interactive prompt**
```bash
npx bmad-method install --directory ~/myapp --modules bmm
# Then select "⚠ None - I am not installing any tools" when prompted
```
## Available Flags
### Installation Options
| Flag | Description | Example |
|------|-------------|---------|
| `--directory <path>` | Installation directory | `--directory ~/projects/myapp` |
| `--modules <modules>` | Comma-separated module IDs | `--modules bmm,bmb` |
| `--tools <tools>` | Comma-separated tool/IDE IDs (use "none" to skip) | `--tools claude-code,cursor` or `--tools none` |
| `--custom-content <paths>` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | Action for existing installations | `--action quick-update` |
### Core Configuration
| Flag | Description | Default |
|------|-------------|---------|
| `--user-name <name>` | Name for agents to use | System username |
| `--communication-language <lang>` | Agent communication language | English |
| `--document-output-language <lang>` | Document output language | English |
| `--output-folder <path>` | Output folder path | _bmad-output |
### Other Options
| Flag | Description |
|------|-------------|
| `-y, --yes` | Accept all defaults and skip prompts |
| `-d, --debug` | Enable debug output for manifest generation |
## Action Types
When working with existing installations, use the `--action` flag:
- `install` - Fresh installation (default for new directories)
- `update` - Modify existing installation (change modules/config)
- `quick-update` - Refresh installation without changing configuration
- `compile-agents` - Recompile agents with customizations only
Example:
```bash
npx bmad-method install --action quick-update
```
## Module IDs
Available module IDs for the `--modules` flag:
### Core Modules
- `bmm` - BMad Method Master
- `bmb` - BMad Builder
### External Modules
Check the [BMad registry](https://github.com/bmad-code-org) for available external modules.
## Tool/IDE IDs
Available tool IDs for the `--tools` flag:
- `claude-code` - Claude Code CLI
- `cursor` - Cursor IDE
- `windsurf` - Windsurf IDE
- `vscode` - Visual Studio Code
- `jetbrains` - JetBrains IDEs
- And more...
Run the interactive installer once to see all available tools.
## Examples
### Basic Installation
Install BMM module with Claude Code:
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--tools claude-code \
--user-name "Development Team"
```
### Installation Without Tools
Install without configuring any tools/IDEs:
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--tools none \
--user-name "Development Team"
```
### Full Installation with Multiple Modules
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm,bmb \
--tools claude-code,cursor \
--user-name "John Doe" \
--communication-language English \
--document-output-language English \
--output-folder _output
```
### Update Existing Installation
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--action update \
--modules bmm,bmb,custom-module
```
### Quick Update (Preserve Settings)
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--action quick-update
```
### Installation with Custom Content
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--custom-content ~/my-custom-module,~/another-module \
--tools claude-code
```
### CI/CD Pipeline Installation
```bash
#!/bin/bash
# install-bmad.sh
npx bmad-method install \
--directory "${GITHUB_WORKSPACE}" \
--modules bmm \
--tools claude-code \
--user-name "CI Bot" \
--communication-language English \
--document-output-language English \
--output-folder _bmad-output \
--yes
```
## Environment-Specific Installations
### Development Environment
```bash
npx bmad-method install \
--directory . \
--modules bmm,bmb \
--tools claude-code,cursor \
--user-name "${USER}"
```
### Production Environment
```bash
npx bmad-method install \
--directory /opt/app \
--modules bmm \
--tools claude-code \
--user-name "Production Team" \
--output-folder /var/bmad-output
```
## Validation and Error Handling
BMAD validates all provided flags:
- **Directory**: Must be a valid path with write permissions
- **Modules**: Will warn about invalid module IDs (but won't fail)
- **Tools**: Will warn about invalid tool IDs (but won't fail)
- **Custom Content**: Each path must contain a valid `module.yaml` file
- **Action**: Must be one of: install, update, quick-update, compile-agents
Invalid values will either:
1. Show an error and exit (for critical options like directory)
2. Show a warning and skip (for optional items like custom content)
3. Fall back to interactive prompts (for missing required values)
## Tips and Best Practices
1. **Use absolute paths** for `--directory` to avoid ambiguity
2. **Test flags locally** before using in CI/CD pipelines
3. **Combine with `-y`** for truly unattended installations
4. **Check module availability** by running the interactive installer once
5. **Use `--debug`** flag if you encounter issues during installation
6. **Skip tool configuration** with `--tools none` for server/CI environments where IDEs aren't needed
7. **Partial flags are OK** - Omit flags and let BMAD prompt for missing values interactively
## Troubleshooting
### Installation fails with "Invalid directory"
Check that:
- The directory path exists or its parent exists
- You have write permissions
- The path is absolute or correctly relative to current directory
### Module not found
- Verify the module ID is correct (check available modules in interactive mode)
- External modules may need to be available in the registry
### Custom content path invalid
Ensure each custom content path:
- Points to a directory
- Contains a `module.yaml` file in the root
- Has a `code` field in the `module.yaml`
## Feedback and Issues
If you encounter any issues with non-interactive installation:
1. Run with `--debug` flag for detailed output
2. Try the interactive mode to verify the issue
3. Report issues on GitHub: <https://github.com/bmad-code-org/BMAD-METHOD/issues>

View File

@ -9,7 +9,22 @@ const ui = new UI();
module.exports = {
command: 'install',
description: 'Install BMAD Core agents and tools',
options: [['-d, --debug', 'Enable debug output for manifest generation']],
options: [
['-d, --debug', 'Enable debug output for manifest generation'],
['--directory <path>', 'Installation directory (default: current directory)'],
['--modules <modules>', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'],
[
'--tools <tools>',
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.',
],
['--custom-content <paths>', 'Comma-separated list of paths to custom modules/agents/workflows'],
['--action <type>', 'Action type for existing installations: install, update, quick-update, or compile-agents'],
['--user-name <name>', 'Name for agents to use (default: system username)'],
['--communication-language <lang>', 'Language for agent communication (default: English)'],
['--document-output-language <lang>', 'Language for document output (default: English)'],
['--output-folder <path>', 'Output folder path relative to project root (default: _bmad-output)'],
['-y, --yes', 'Accept all defaults and skip prompts where possible'],
],
action: async (options) => {
try {
// Set debug flag as environment variable for all components
@ -18,7 +33,7 @@ module.exports = {
console.log(chalk.cyan('Debug mode enabled\n'));
}
const config = await ui.promptInstall();
const config = await ui.promptInstall(options);
// Handle cancel
if (config.actionType === 'cancel') {

View File

@ -26,9 +26,10 @@ const choiceUtils = { Separator };
class UI {
/**
* Prompt for installation configuration
* @param {Object} options - Command-line options from install command
* @returns {Object} Installation configuration
*/
async promptInstall() {
async promptInstall(options = {}) {
CLIUtils.displayLogo();
// Display version-specific start message from install-messages.yaml
@ -36,7 +37,20 @@ class UI {
const messageLoader = new MessageLoader();
messageLoader.displayStartMessage();
const confirmedDirectory = await this.getConfirmedDirectory();
// Get directory from options or prompt
let confirmedDirectory;
if (options.directory) {
// Use provided directory from command-line
const expandedDir = this.expandUserPath(options.directory);
const validation = this.validateDirectorySync(expandedDir);
if (validation !== true) {
throw new Error(`Invalid directory: ${validation}`);
}
confirmedDirectory = expandedDir;
console.log(chalk.cyan('Using directory from command-line:'), chalk.bold(confirmedDirectory));
} else {
confirmedDirectory = await this.getConfirmedDirectory();
}
// Preflight: Check for legacy BMAD v4 footprints immediately after getting directory
const { Detector } = require('../installers/lib/core/detector');
@ -218,11 +232,21 @@ class UI {
// Common actions
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
actionType = await prompts.select({
message: 'How would you like to proceed?',
choices: choices,
default: choices[0].value,
});
// Check if action is provided via command-line
if (options.action) {
const validActions = choices.map((c) => c.value);
if (!validActions.includes(options.action)) {
throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`);
}
actionType = options.action;
console.log(chalk.cyan('Using action from command-line:'), chalk.bold(actionType));
} else {
actionType = await prompts.select({
message: 'How would you like to proceed?',
choices: choices,
default: choices[0].value,
});
}
// Handle quick update separately
if (actionType === 'quick-update') {
@ -253,30 +277,94 @@ class UI {
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
// Unified module selection - all modules in one grouped multiselect
let selectedModules = await this.selectAllModules(installedModuleIds);
let selectedModules;
if (options.modules) {
// Use modules from command-line
selectedModules = options.modules
.split(',')
.map((m) => m.trim())
.filter(Boolean);
console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', ')));
} else {
selectedModules = await this.selectAllModules(installedModuleIds);
}
// After module selection, ask about custom modules
console.log('');
const changeCustomModules = await prompts.confirm({
message: 'Modify custom modules, agents, or workflows?',
default: false,
});
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
if (changeCustomModules) {
customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
} else {
// Preserve existing custom modules if user doesn't want to modify them
const { Installer } = require('../installers/lib/core/installer');
const installer = new Installer();
const { bmadDir } = await installer.findBmadDir(confirmedDirectory);
const cacheDir = path.join(bmadDir, '_config', 'custom');
if (await fs.pathExists(cacheDir)) {
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
customModuleResult.selectedCustomModules.push(entry.name);
if (options.customContent) {
// Use custom content from command-line
const paths = options.customContent
.split(',')
.map((p) => p.trim())
.filter(Boolean);
console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', ')));
// Build custom content config similar to promptCustomContentSource
const customPaths = [];
const selectedModuleIds = [];
for (const customPath of paths) {
const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath);
if (validation !== true) {
console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`));
continue;
}
// Read module metadata
let moduleMeta;
try {
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
const yaml = require('yaml');
moduleMeta = yaml.parse(moduleYaml);
} catch (error) {
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`));
continue;
}
if (!moduleMeta.code) {
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`));
continue;
}
customPaths.push(expandedPath);
selectedModuleIds.push(moduleMeta.code);
}
if (customPaths.length > 0) {
customModuleResult = {
selectedCustomModules: selectedModuleIds,
customContentConfig: {
hasCustomContent: true,
paths: customPaths,
selectedModuleIds: selectedModuleIds,
},
};
}
} else {
const changeCustomModules = await prompts.confirm({
message: 'Modify custom modules, agents, or workflows?',
default: false,
});
if (changeCustomModules) {
customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
} else {
// Preserve existing custom modules if user doesn't want to modify them
const { Installer } = require('../installers/lib/core/installer');
const installer = new Installer();
const { bmadDir } = await installer.findBmadDir(confirmedDirectory);
const cacheDir = path.join(bmadDir, '_config', 'custom');
if (await fs.pathExists(cacheDir)) {
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
customModuleResult.selectedCustomModules.push(entry.name);
}
}
}
}
@ -288,9 +376,9 @@ class UI {
}
// Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory);
const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
const coreConfig = await this.collectCoreConfig(confirmedDirectory, options);
return {
actionType: 'update',
@ -309,16 +397,76 @@ class UI {
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
// Unified module selection - all modules in one grouped multiselect
let selectedModules = await this.selectAllModules(installedModuleIds);
let selectedModules;
if (options.modules) {
// Use modules from command-line
selectedModules = options.modules
.split(',')
.map((m) => m.trim())
.filter(Boolean);
console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', ')));
} else {
selectedModules = await this.selectAllModules(installedModuleIds);
}
// Ask about custom content (local modules/agents/workflows)
const wantsCustomContent = await prompts.confirm({
message: 'Add custom modules, agents, or workflows from your computer?',
default: false,
});
if (options.customContent) {
// Use custom content from command-line
const paths = options.customContent
.split(',')
.map((p) => p.trim())
.filter(Boolean);
console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', ')));
if (wantsCustomContent) {
customContentConfig = await this.promptCustomContentSource();
// Build custom content config similar to promptCustomContentSource
const customPaths = [];
const selectedModuleIds = [];
for (const customPath of paths) {
const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath);
if (validation !== true) {
console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`));
continue;
}
// Read module metadata
let moduleMeta;
try {
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
const yaml = require('yaml');
moduleMeta = yaml.parse(moduleYaml);
} catch (error) {
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`));
continue;
}
if (!moduleMeta.code) {
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`));
continue;
}
customPaths.push(expandedPath);
selectedModuleIds.push(moduleMeta.code);
}
if (customPaths.length > 0) {
customContentConfig = {
hasCustomContent: true,
paths: customPaths,
selectedModuleIds: selectedModuleIds,
};
}
} else if (!options.yes) {
const wantsCustomContent = await prompts.confirm({
message: 'Add custom modules, agents, or workflows from your computer?',
default: false,
});
if (wantsCustomContent) {
customContentConfig = await this.promptCustomContentSource();
}
}
// Add custom content modules if any were selected
@ -327,8 +475,8 @@ class UI {
}
selectedModules = selectedModules.filter((m) => m !== 'core');
let toolSelection = await this.promptToolSelection(confirmedDirectory);
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
const coreConfig = await this.collectCoreConfig(confirmedDirectory, options);
return {
actionType: 'install',
@ -345,9 +493,10 @@ class UI {
/**
* Prompt for tool/IDE selection (called after module configuration)
* @param {string} projectDir - Project directory to check for existing IDEs
* @param {Object} options - Command-line options
* @returns {Object} Tool configuration
*/
async promptToolSelection(projectDir) {
async promptToolSelection(projectDir, options = {}) {
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
const { Detector } = require('../installers/lib/core/detector');
const { Installer } = require('../installers/lib/core/installer');
@ -433,13 +582,37 @@ class UI {
let selectedIdes = [];
selectedIdes = await prompts.groupMultiselect({
message: `Select tools to configure ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
options: groupedOptions,
initialValues: initialValues.length > 0 ? initialValues : undefined,
required: true,
selectableGroups: false,
});
// Check if tools are provided via command-line
if (options.tools) {
// Check for explicit "none" value to skip tool installation
if (options.tools.toLowerCase() === 'none') {
console.log(chalk.cyan('Skipping tool configuration (--tools none)'));
selectedIdes = [];
} else {
selectedIdes = options.tools
.split(',')
.map((t) => t.trim())
.filter(Boolean);
console.log(chalk.cyan('Using tools from command-line:'), chalk.bold(selectedIdes.join(', ')));
}
} else if (options.yes) {
// If --yes flag is set, skip tool prompt and use previously configured tools or empty
if (initialValues.length > 0) {
selectedIdes = initialValues;
console.log(chalk.cyan('Using previously configured tools (--yes flag):'), chalk.bold(selectedIdes.join(', ')));
} else {
console.log(chalk.cyan('Skipping tool configuration (--yes flag, no previous tools)'));
selectedIdes = [];
}
} else {
selectedIdes = await prompts.groupMultiselect({
message: `Select tools to configure ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
options: groupedOptions,
initialValues: initialValues.length > 0 ? initialValues : undefined,
required: true,
selectableGroups: false,
});
}
// If user selected both "__NONE__" and other tools, honor the "None" choice
if (selectedIdes && selectedIdes.includes('__NONE__') && selectedIdes.length > 1) {
@ -542,15 +715,75 @@ class UI {
/**
* Collect core configuration
* @param {string} directory - Installation directory
* @param {Object} options - Command-line options
* @returns {Object} Core configuration
*/
async collectCoreConfig(directory) {
async collectCoreConfig(directory, options = {}) {
const { ConfigCollector } = require('../installers/lib/core/config-collector');
const configCollector = new ConfigCollector();
// Load existing configs first if they exist
await configCollector.loadExistingConfig(directory);
// Now collect with existing values as defaults (false = don't skip loading, true = skip completion message)
await configCollector.collectModuleConfig('core', directory, false, true);
// If options are provided, set them directly
if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) {
const coreConfig = {};
if (options.userName) {
coreConfig.user_name = options.userName;
console.log(chalk.cyan('Using user name from command-line:'), chalk.bold(options.userName));
}
if (options.communicationLanguage) {
coreConfig.communication_language = options.communicationLanguage;
console.log(chalk.cyan('Using communication language from command-line:'), chalk.bold(options.communicationLanguage));
}
if (options.documentOutputLanguage) {
coreConfig.document_output_language = options.documentOutputLanguage;
console.log(chalk.cyan('Using document output language from command-line:'), chalk.bold(options.documentOutputLanguage));
}
if (options.outputFolder) {
coreConfig.output_folder = options.outputFolder;
console.log(chalk.cyan('Using output folder from command-line:'), chalk.bold(options.outputFolder));
}
// Load existing config to merge with provided options
await configCollector.loadExistingConfig(directory);
// Merge provided options with existing config (or defaults)
const existingConfig = configCollector.collectedConfig.core || {};
configCollector.collectedConfig.core = { ...existingConfig, ...coreConfig };
// If not all options are provided, collect the missing ones interactively (unless --yes flag)
if (
!options.yes &&
(!options.userName || !options.communicationLanguage || !options.documentOutputLanguage || !options.outputFolder)
) {
await configCollector.collectModuleConfig('core', directory, false, true);
}
} else if (options.yes) {
// Use all defaults when --yes flag is set
await configCollector.loadExistingConfig(directory);
const existingConfig = configCollector.collectedConfig.core || {};
// If no existing config, use defaults
if (Object.keys(existingConfig).length === 0) {
let safeUsername;
try {
safeUsername = os.userInfo().username;
} catch {
safeUsername = process.env.USER || process.env.USERNAME || 'User';
}
const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1);
configCollector.collectedConfig.core = {
user_name: defaultUsername,
communication_language: 'English',
document_output_language: 'English',
output_folder: '_bmad-output',
};
console.log(chalk.cyan('Using default configuration (--yes flag)'));
}
} else {
// Load existing configs first if they exist
await configCollector.loadExistingConfig(directory);
// Now collect with existing values as defaults (false = don't skip loading, true = skip completion message)
await configCollector.collectModuleConfig('core', directory, false, true);
}
const coreConfig = configCollector.collectedConfig.core;
// Ensure we always have a core config object, even if empty