feat: add non-interactive installation support (#1520)

* feat: add non-interactive installation support

Add command-line flags to support non-interactive installation for CI/CD
pipelines and automated deployments:

- --directory: Installation directory
- --modules: Comma-separated module IDs
- --tools: Tool/IDE IDs (use "none" to skip)
- --custom-content: Custom module paths
- --action: Action type for existing installations
- --user-name, --communication-language, --document-output-language, --output-folder: Core config
- -y, --yes: Accept all defaults

When flags are provided, prompts are skipped. Missing values gracefully
fall back to interactive prompts.

* fix: complete non-interactive installation support

- Fix validation checks using truthy instead of !== true
- Add skipPrompts flag to skip module config prompts with --yes
- Add getDefaultModules() for automatic module selection with --yes
- Fix IDE selection to use array check instead of length check

Co-Authored-By: AiderDesk <https://github.com/hotovo/aider-desk>

---------

Co-authored-by: Brian <bmadcode@gmail.com>
This commit is contained in:
Vladimir Hrusovsky 2026-02-06 02:13:11 +01:00 committed by GitHub
parent 068a9dc45a
commit 22601f8198
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 714 additions and 88 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. 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: > **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?` - `/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 = { module.exports = {
command: 'install', command: 'install',
description: 'Install BMAD Core agents and tools', 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) => { action: async (options) => {
try { try {
// Set debug flag as environment variable for all components // Set debug flag as environment variable for all components
@ -18,7 +33,7 @@ module.exports = {
console.log(chalk.cyan('Debug mode enabled\n')); console.log(chalk.cyan('Debug mode enabled\n'));
} }
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

@ -136,10 +136,12 @@ class ConfigCollector {
* @param {string} projectDir - Target project directory * @param {string} projectDir - Target project directory
* @param {Object} options - Additional options * @param {Object} options - Additional options
* @param {Map} options.customModulePaths - Map of module ID to source path for custom modules * @param {Map} options.customModulePaths - Map of module ID to source path for custom modules
* @param {boolean} options.skipPrompts - Skip prompts and use defaults (for --yes flag)
*/ */
async collectAllConfigurations(modules, projectDir, options = {}) { async collectAllConfigurations(modules, projectDir, options = {}) {
// Store custom module paths for use in collectModuleConfig // Store custom module paths for use in collectModuleConfig
this.customModulePaths = options.customModulePaths || new Map(); this.customModulePaths = options.customModulePaths || new Map();
this.skipPrompts = options.skipPrompts || false;
await this.loadExistingConfig(projectDir); await this.loadExistingConfig(projectDir);
// Check if core was already collected (e.g., in early collection phase) // Check if core was already collected (e.g., in early collection phase)
@ -583,47 +585,60 @@ class ConfigCollector {
// If there are questions to ask, prompt for accepting defaults vs customizing // If there are questions to ask, prompt for accepting defaults vs customizing
if (questions.length > 0) { if (questions.length > 0) {
const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`; const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
console.log();
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); // Skip prompts mode: use all defaults without asking
let customize = true; if (this.skipPrompts) {
if (moduleName === 'core') { console.log(chalk.cyan('Using default configuration for'), chalk.magenta(moduleDisplayName));
// Core module: no confirm prompt, so add spacing manually to match visual style // Use defaults for all questions
console.log(chalk.gray('│')); for (const question of questions) {
const hasDefault = question.default !== undefined && question.default !== null && question.default !== '';
if (hasDefault && typeof question.default !== 'function') {
allAnswers[question.name] = question.default;
}
}
} else { } else {
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing) console.log();
const customizeAnswer = await prompts.prompt([ console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
{ let customize = true;
type: 'confirm', if (moduleName === 'core') {
name: 'customize', // Core module: no confirm prompt, so add spacing manually to match visual style
message: 'Accept Defaults (no to customize)?', console.log(chalk.gray('│'));
default: true, } else {
}, // Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
]); const customizeAnswer = await prompts.prompt([
customize = customizeAnswer.customize; {
} type: 'confirm',
name: 'customize',
message: 'Accept Defaults (no to customize)?',
default: true,
},
]);
customize = customizeAnswer.customize;
}
if (customize && moduleName !== 'core') { if (customize && moduleName !== 'core') {
// Accept defaults - only ask questions that have NO default value // Accept defaults - only ask questions that have NO default value
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === ''); const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
if (questionsWithoutDefaults.length > 0) { if (questionsWithoutDefaults.length > 0) {
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`)); console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
const promptedAnswers = await prompts.prompt(questionsWithoutDefaults); const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
Object.assign(allAnswers, promptedAnswers);
}
// For questions with defaults that weren't asked, we need to process them with their default values
const questionsWithDefaults = questions.filter((q) => q.default !== undefined && q.default !== null && q.default !== '');
for (const question of questionsWithDefaults) {
// Skip function defaults - these are dynamic and will be evaluated later
if (typeof question.default === 'function') {
continue;
}
allAnswers[question.name] = question.default;
}
} else {
const promptedAnswers = await prompts.prompt(questions);
Object.assign(allAnswers, promptedAnswers); Object.assign(allAnswers, promptedAnswers);
} }
// For questions with defaults that weren't asked, we need to process them with their default values
const questionsWithDefaults = questions.filter((q) => q.default !== undefined && q.default !== null && q.default !== '');
for (const question of questionsWithDefaults) {
// Skip function defaults - these are dynamic and will be evaluated later
if (typeof question.default === 'function') {
continue;
}
allAnswers[question.name] = question.default;
}
} else {
const promptedAnswers = await prompts.prompt(questions);
Object.assign(allAnswers, promptedAnswers);
} }
} }

View File

@ -353,11 +353,13 @@ class Installer {
const modulesWithoutCore = allModulesForConfig.filter((m) => m !== 'core'); const modulesWithoutCore = allModulesForConfig.filter((m) => m !== 'core');
moduleConfigs = await this.configCollector.collectAllConfigurations(modulesWithoutCore, path.resolve(config.directory), { moduleConfigs = await this.configCollector.collectAllConfigurations(modulesWithoutCore, path.resolve(config.directory), {
customModulePaths, customModulePaths,
skipPrompts: config.skipPrompts,
}); });
} else { } else {
// Core not collected yet, include it // Core not collected yet, include it
moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), { moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), {
customModulePaths, customModulePaths,
skipPrompts: config.skipPrompts,
}); });
} }
} }
@ -680,7 +682,8 @@ class Installer {
} else { } else {
// Pass pre-selected IDEs from early prompt (if available) // Pass pre-selected IDEs from early prompt (if available)
// This allows IDE selection to happen before file copying, improving UX // This allows IDE selection to happen before file copying, improving UX
const preSelectedIdes = config.ides && config.ides.length > 0 ? config.ides : null; // Use config.ides if it's an array (even if empty), null means prompt
const preSelectedIdes = Array.isArray(config.ides) ? config.ides : null;
toolSelection = await this.collectToolConfigurations( toolSelection = await this.collectToolConfigurations(
path.resolve(config.directory), path.resolve(config.directory),
config.modules, config.modules,

View File

@ -26,9 +26,10 @@ const choiceUtils = { Separator };
class UI { class UI {
/** /**
* Prompt for installation configuration * Prompt for installation configuration
* @param {Object} options - Command-line options from install command
* @returns {Object} Installation configuration * @returns {Object} Installation configuration
*/ */
async promptInstall() { async promptInstall(options = {}) {
CLIUtils.displayLogo(); CLIUtils.displayLogo();
// Display version-specific start message from install-messages.yaml // Display version-specific start message from install-messages.yaml
@ -36,7 +37,20 @@ class UI {
const messageLoader = new MessageLoader(); const messageLoader = new MessageLoader();
messageLoader.displayStartMessage(); 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) {
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 // Preflight: Check for legacy BMAD v4 footprints immediately after getting directory
const { Detector } = require('../installers/lib/core/detector'); const { Detector } = require('../installers/lib/core/detector');
@ -218,11 +232,21 @@ class UI {
// Common actions // Common actions
choices.push({ name: 'Modify BMAD Installation', value: 'update' }); choices.push({ name: 'Modify BMAD Installation', value: 'update' });
actionType = await prompts.select({ // Check if action is provided via command-line
message: 'How would you like to proceed?', if (options.action) {
choices: choices, const validActions = choices.map((c) => c.value);
default: choices[0].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 // Handle quick update separately
if (actionType === 'quick-update') { if (actionType === 'quick-update') {
@ -253,30 +277,94 @@ class UI {
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`)); console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
// Unified module selection - all modules in one grouped multiselect // 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 // After module selection, ask about custom modules
console.log(''); console.log('');
const changeCustomModules = await prompts.confirm({
message: 'Modify custom modules, agents, or workflows?',
default: false,
});
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: 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 (options.customContent) {
if (await fs.pathExists(cacheDir)) { // Use custom content from command-line
const entries = await fs.readdir(cacheDir, { withFileTypes: true }); const paths = options.customContent
for (const entry of entries) { .split(',')
if (entry.isDirectory()) { .map((p) => p.trim())
customModuleResult.selectedCustomModules.push(entry.name); .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) {
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 // 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 { return {
actionType: 'update', actionType: 'update',
@ -309,16 +397,80 @@ class UI {
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory); const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
// Unified module selection - all modules in one grouped multiselect // 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 if (options.yes) {
// Use default modules when --yes flag is set
selectedModules = await this.getDefaultModules(installedModuleIds);
console.log(chalk.cyan('Using default modules (--yes flag):'), chalk.bold(selectedModules.join(', ')));
} else {
selectedModules = await this.selectAllModules(installedModuleIds);
}
// Ask about custom content (local modules/agents/workflows) // Ask about custom content (local modules/agents/workflows)
const wantsCustomContent = await prompts.confirm({ if (options.customContent) {
message: 'Add custom modules, agents, or workflows from your computer?', // Use custom content from command-line
default: false, 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) { // Build custom content config similar to promptCustomContentSource
customContentConfig = await this.promptCustomContentSource(); const customPaths = [];
const selectedModuleIds = [];
for (const customPath of paths) {
const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath);
if (validation) {
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 // Add custom content modules if any were selected
@ -327,8 +479,8 @@ class UI {
} }
selectedModules = selectedModules.filter((m) => m !== 'core'); selectedModules = selectedModules.filter((m) => m !== 'core');
let toolSelection = await this.promptToolSelection(confirmedDirectory); let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
const coreConfig = await this.collectCoreConfig(confirmedDirectory); const coreConfig = await this.collectCoreConfig(confirmedDirectory, options);
return { return {
actionType: 'install', actionType: 'install',
@ -339,6 +491,7 @@ class UI {
skipIde: toolSelection.skipIde, skipIde: toolSelection.skipIde,
coreConfig: coreConfig, coreConfig: coreConfig,
customContent: customContentConfig, customContent: customContentConfig,
skipPrompts: options.yes || false,
}; };
} }
@ -348,9 +501,10 @@ class UI {
* 1. Recommended tools - standard multiselect for 3 preferred tools * 1. Recommended tools - standard multiselect for 3 preferred tools
* 2. Additional tools - autocompleteMultiselect with search capability * 2. Additional tools - autocompleteMultiselect with search capability
* @param {string} projectDir - Project directory to check for existing IDEs * @param {string} projectDir - Project directory to check for existing IDEs
* @param {Object} options - Command-line options
* @returns {Object} Tool configuration * @returns {Object} Tool configuration
*/ */
async promptToolSelection(projectDir) { async promptToolSelection(projectDir, options = {}) {
// Check for existing configured IDEs - use findBmadDir to detect custom folder names // Check for existing configured IDEs - use findBmadDir to detect custom folder names
const { Detector } = require('../installers/lib/core/detector'); const { Detector } = require('../installers/lib/core/detector');
const { Installer } = require('../installers/lib/core/installer'); const { Installer } = require('../installers/lib/core/installer');
@ -449,7 +603,37 @@ class UI {
}; };
}); });
const selectedIdes = await prompts.autocompleteMultiselect({ let selectedIdes = [];
// 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)'));
return { ides: [], skipIde: true };
} else {
selectedIdes = options.tools
.split(',')
.map((t) => t.trim())
.filter(Boolean);
console.log(chalk.cyan('Using tools from command-line:'), chalk.bold(selectedIdes.join(', ')));
this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false };
}
} else if (options.yes) {
// If --yes flag is set, skip tool prompt and use previously configured tools or empty
if (configuredIdes.length > 0) {
console.log(chalk.cyan('Using previously configured tools (--yes flag):'), chalk.bold(configuredIdes.join(', ')));
this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
return { ides: configuredIdes, skipIde: false };
} else {
console.log(chalk.cyan('Skipping tool configuration (--yes flag, no previous tools)'));
return { ides: [], skipIde: true };
}
}
// Interactive mode
const interactiveSelectedIdes = await prompts.autocompleteMultiselect({
message: 'Select tools:', message: 'Select tools:',
options: allToolOptions, options: allToolOptions,
initialValues: configuredIdes.length > 0 ? configuredIdes : undefined, initialValues: configuredIdes.length > 0 ? configuredIdes : undefined,
@ -457,12 +641,12 @@ class UI {
maxItems: 8, maxItems: 8,
}); });
const allSelectedIdes = selectedIdes || []; selectedIdes = interactiveSelectedIdes || [];
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// STEP 3: Confirm if no tools selected // STEP 3: Confirm if no tools selected
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
if (allSelectedIdes.length === 0) { if (selectedIdes.length === 0) {
console.log(''); console.log('');
const confirmNoTools = await prompts.confirm({ const confirmNoTools = await prompts.confirm({
message: 'No tools selected. Continue without installing any tools?', message: 'No tools selected. Continue without installing any tools?',
@ -481,11 +665,11 @@ class UI {
} }
// Display selected tools // Display selected tools
this.displaySelectedTools(allSelectedIdes, preferredIdes, allTools); this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { return {
ides: allSelectedIdes, ides: selectedIdes,
skipIde: allSelectedIdes.length === 0, skipIde: selectedIdes.length === 0,
}; };
} }
@ -573,15 +757,75 @@ class UI {
/** /**
* Collect core configuration * Collect core configuration
* @param {string} directory - Installation directory * @param {string} directory - Installation directory
* @param {Object} options - Command-line options
* @returns {Object} Core configuration * @returns {Object} Core configuration
*/ */
async collectCoreConfig(directory) { async collectCoreConfig(directory, options = {}) {
const { ConfigCollector } = require('../installers/lib/core/config-collector'); const { ConfigCollector } = require('../installers/lib/core/config-collector');
const configCollector = new ConfigCollector(); const configCollector = new ConfigCollector();
// Load existing configs first if they exist
await configCollector.loadExistingConfig(directory); // If options are provided, set them directly
// Now collect with existing values as defaults (false = don't skip loading, true = skip completion message) if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) {
await configCollector.collectModuleConfig('core', directory, false, true); 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; const coreConfig = configCollector.collectedConfig.core;
// Ensure we always have a core config object, even if empty // Ensure we always have a core config object, even if empty
@ -885,6 +1129,33 @@ class UI {
return selected ? selected.filter((m) => m !== '__NONE__') : []; return selected ? selected.filter((m) => m !== '__NONE__') : [];
} }
/**
* Get default modules for non-interactive mode
* @param {Set} installedModuleIds - Already installed module IDs
* @returns {Array} Default module codes
*/
async getDefaultModules(installedModuleIds = new Set()) {
const { ModuleManager } = require('../installers/lib/modules/manager');
const moduleManager = new ModuleManager();
const { modules: localModules } = await moduleManager.listAvailable();
const defaultModules = [];
// Add default-selected local modules (typically BMM)
for (const mod of localModules) {
if (mod.defaultSelected === true || installedModuleIds.has(mod.id)) {
defaultModules.push(mod.id);
}
}
// If no defaults found, use 'bmm' as the fallback default
if (defaultModules.length === 0) {
defaultModules.push('bmm');
}
return defaultModules;
}
/** /**
* Prompt for directory selection * Prompt for directory selection
* @returns {Object} Directory answer from prompt * @returns {Object} Directory answer from prompt