Merge branch 'main' into docs/add-pr-template

This commit is contained in:
Michael Pursifull 2026-02-06 01:20:42 -06:00 committed by GitHub
commit 2bf04c003c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 919 additions and 274 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

@ -147,7 +147,7 @@ your-project/
| **Concept** | `what-are-agents.md` | | **Concept** | `what-are-agents.md` |
| **Feature** | `quick-flow.md` | | **Feature** | `quick-flow.md` |
| **Philosophy** | `why-solutioning-matters.md` | | **Philosophy** | `why-solutioning-matters.md` |
| **FAQ** | `brownfield-faq.md` | | **FAQ** | `established-projects-faq.md` |
### General Template ### General Template
@ -325,7 +325,7 @@ Add italic context at definition start for limited-scope terms:
- `*BMad Method/Enterprise.*` - `*BMad Method/Enterprise.*`
- `*Phase N.*` - `*Phase N.*`
- `*BMGD.*` - `*BMGD.*`
- `*Brownfield.*` - `*Established projects.*`
### Glossary Checklist ### Glossary Checklist

View File

@ -1,55 +0,0 @@
---
title: "Brownfield Development FAQ"
description: Common questions about brownfield development in the BMad Method
---
Quick answers to common questions about brownfield (existing codebase) development in the BMad Method (BMM).
## Questions
- [Questions](#questions)
- [What is brownfield vs greenfield?](#what-is-brownfield-vs-greenfield)
- [Do I have to run document-project for brownfield?](#do-i-have-to-run-document-project-for-brownfield)
- [What if I forget to run document-project?](#what-if-i-forget-to-run-document-project)
- [Can I use Quick Spec Flow for brownfield projects?](#can-i-use-quick-spec-flow-for-brownfield-projects)
- [What if my existing code doesn't follow best practices?](#what-if-my-existing-code-doesnt-follow-best-practices)
### What is brownfield vs greenfield?
- **Greenfield** — New project, starting from scratch, clean slate
- **Brownfield** — Existing project, working with established codebase and patterns
### Do I have to run document-project for brownfield?
Highly recommended, especially if:
- No existing documentation
- Documentation is outdated
- AI agents need context about existing code
You can skip it if you have comprehensive, up-to-date documentation including `docs/index.md` or will use other tools or techniques to aid in discovery for the agent to build on an existing system.
### What if I forget to run document-project?
Don't worry about it - you can do it at any time. You can even do it during or after a project to help keep docs up to date.
### Can I use Quick Spec Flow for brownfield projects?
Yes! Quick Spec Flow works great for brownfield. It will:
- Auto-detect your existing stack
- Analyze brownfield code patterns
- Detect conventions and ask for confirmation
- Generate context-rich tech-spec that respects existing code
Perfect for bug fixes and small features in existing codebases.
### What if my existing code doesn't follow best practices?
Quick Spec Flow detects your conventions and asks: "Should I follow these existing conventions?" You decide:
- **Yes** → Maintain consistency with current codebase
- **No** → Establish new standards (document why in tech-spec)
BMM respects your choice — it won't force modernization, but it will offer it.
**Have a question not answered here?** Please [open an issue](https://github.com/bmad-code-org/BMAD-METHOD/issues) or ask in [Discord](https://discord.gg/gk8jAdXWmj) so we can add it!

View File

@ -0,0 +1,48 @@
---
title: "Established Projects FAQ"
description: Common questions about using BMad Method on established projects
---
Quick answers to common questions about working on established projects with the BMad Method (BMM).
## Questions
- [Do I have to run document-project first?](#do-i-have-to-run-document-project-first)
- [What if I forget to run document-project?](#what-if-i-forget-to-run-document-project)
- [Can I use Quick Flow for established projects?](#can-i-use-quick-flow-for-established-projects)
- [What if my existing code doesn't follow best practices?](#what-if-my-existing-code-doesnt-follow-best-practices)
### Do I have to run document-project first?
Highly recommended, especially if:
- No existing documentation
- Documentation is outdated
- AI agents need context about existing code
You can skip it if you have comprehensive, up-to-date documentation including `docs/index.md` or will use other tools or techniques to aid in discovery for the agent to build on an existing system.
### What if I forget to run document-project?
Don't worry about it - you can do it at any time. You can even do it during or after a project to help keep docs up to date.
### Can I use Quick Flow for established projects?
Yes! Quick Flow works great for established projects. It will:
- Auto-detect your existing stack
- Analyze existing code patterns
- Detect conventions and ask for confirmation
- Generate context-rich tech-spec that respects existing code
Perfect for bug fixes and small features in existing codebases.
### What if my existing code doesn't follow best practices?
Quick Flow detects your conventions and asks: "Should I follow these existing conventions?" You decide:
- **Yes** → Maintain consistency with current codebase
- **No** → Establish new standards (document why in tech-spec)
BMM respects your choice — it won't force modernization, but it will offer it.
**Have a question not answered here?** Please [open an issue](https://github.com/bmad-code-org/BMAD-METHOD/issues) or ask in [Discord](https://discord.gg/gk8jAdXWmj) so we can add it!

View File

@ -1,15 +1,11 @@
--- ---
title: "Brownfield Development" title: "Established Projects"
description: How to use BMad Method on existing codebases description: How to use BMad Method on existing codebases
--- ---
Use BMad Method effectively when working on existing projects and legacy codebases. Use BMad Method effectively when working on existing projects and legacy codebases.
## What is Brownfield Development? This guide covers the essential workflow for onboarding to existing projects with BMad Method.
**Brownfield** refers to working on existing projects with established codebases and patterns, as opposed to **greenfield** which means starting from scratch with a clean slate.
This guide covers the essential workflow for onboarding to brownfield projects with BMad Method.
:::note[Prerequisites] :::note[Prerequisites]
- BMad Method installed (`npx bmad-method install`) - BMad Method installed (`npx bmad-method install`)
@ -80,5 +76,5 @@ Pay close attention here to prevent reinventing the wheel or making decisions th
## More Information ## More Information
- **[Quick Fix in Brownfield](/docs/how-to/brownfield/quick-fix-in-brownfield.md)** - Bug fixes and ad-hoc changes - **[Quick Fixes](/docs/how-to/quick-fixes.md)** - Bug fixes and ad-hoc changes
- **[Brownfield FAQ](/docs/explanation/brownfield-faq.md)** - Common questions about brownfield development - **[Established Projects FAQ](/docs/explanation/established-projects-faq.md)** - Common questions about working on established projects

View File

@ -1,6 +1,6 @@
--- ---
title: "How to Make Quick Fixes in Brownfield Projects" title: "Quick Fixes"
description: How to make quick fixes and ad-hoc changes in brownfield projects description: How to make quick fixes and ad-hoc changes
--- ---
Use the **DEV agent** directly for bug fixes, refactorings, or small targeted changes that don't require the full BMad method or Quick Flow. Use the **DEV agent** directly for bug fixes, refactorings, or small targeted changes that don't require the full BMad method or Quick Flow.

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

@ -73,7 +73,7 @@ Skip phases 1-3 for small, well-understood work.
Each document becomes context for the next phase. The PRD tells the architect what constraints matter. The architecture tells the dev agent which patterns to follow. Story files give focused, complete context for implementation. Without this structure, agents make inconsistent decisions. Each document becomes context for the next phase. The PRD tells the architect what constraints matter. The architecture tells the dev agent which patterns to follow. Story files give focused, complete context for implementation. Without this structure, agents make inconsistent decisions.
For brownfield projects, `document-project` creates or updates `project-context.md` - what exists in the codebase and the rules all implementation workflows must observe. Run it just before Phase 4, and again when something significant changes - structure, architecture, or those rules. You can also edit `project-context.md` by hand. For established projects, `document-project` creates or updates `project-context.md` - what exists in the codebase and the rules all implementation workflows must observe. Run it just before Phase 4, and again when something significant changes - structure, architecture, or those rules. You can also edit `project-context.md` by hand.
All implementation workflows load `project-context.md` if it exists. Additional context per workflow: All implementation workflows load `project-context.md` if it exists. Additional context per workflow:

View File

@ -54,13 +54,15 @@ Determine what was just completed:
1. **Load catalog** — Load `{project-root}/_bmad/_config/bmad-help.csv` 1. **Load catalog** — Load `{project-root}/_bmad/_config/bmad-help.csv`
2. **Resolve output locations** — Scan each folder under `_bmad/` (except `_config`) for `config.yaml`. For each workflow row, resolve its `output-location` variables against that module's config so artifact paths can be searched. 2. **Resolve output locations and config** — Scan each folder under `_bmad/` (except `_config`) for `config.yaml`. For each workflow row, resolve its `output-location` variables against that module's config so artifact paths can be searched. Also extract `communication_language` and `project_knowledge` from each scanned module's config.
3. **Detect active module** — Use MODULE DETECTION above 3. **Ground in project knowledge** — If `project_knowledge` resolves to an existing path, read available documentation files (architecture docs, project overview, tech stack references) for grounding context. Use discovered project facts when composing any project-specific output. Never fabricate project-specific details — if documentation is unavailable, state so.
4. **Analyze input** — Task may provide a workflow name/code, conversational phrase, or nothing. Infer what was just completed using INPUT ANALYSIS above. 4. **Detect active module** — Use MODULE DETECTION above
5. **Present recommendations** — Show next steps based on: 5. **Analyze input** — Task may provide a workflow name/code, conversational phrase, or nothing. Infer what was just completed using INPUT ANALYSIS above.
6. **Present recommendations** — Show next steps based on:
- Completed workflows detected - Completed workflows detected
- Phase/sequence ordering (ROUTING RULES) - Phase/sequence ordering (ROUTING RULES)
- Artifact presence - Artifact presence
@ -74,9 +76,10 @@ Determine what was just completed:
- **Agent** title and display name from the CSV (e.g., "🎨 Alex (Designer)") - **Agent** title and display name from the CSV (e.g., "🎨 Alex (Designer)")
- Brief **description** - Brief **description**
6. **Additional guidance to convey**: 7. **Additional guidance to convey**:
- Present all output in `{communication_language}`
- Run each workflow in a **fresh context window** - Run each workflow in a **fresh context window**
- For **validation workflows**: recommend using a different high-quality LLM if available - For **validation workflows**: recommend using a different high-quality LLM if available
- For conversational requests: match the user's tone while presenting clearly - For conversational requests: match the user's tone while presenting clearly
7. Return to the calling process after presenting recommendations. 8. Return to the calling process after presenting recommendations.

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

@ -1,7 +1,10 @@
const path = require('node:path'); const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const yaml = require('yaml');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
/** /**
* KiloCode IDE setup handler * KiloCode IDE setup handler
@ -22,76 +25,94 @@ class KiloSetup extends BaseIdeSetup {
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`)); console.log(chalk.cyan(`Setting up ${this.name}...`));
// Check for existing .kilocodemodes file // Clean up any old BMAD installation first
await this.cleanup(projectDir);
// Load existing config (may contain non-BMAD modes and other settings)
const kiloModesPath = path.join(projectDir, this.configFile); const kiloModesPath = path.join(projectDir, this.configFile);
let existingModes = []; let config = {};
let existingContent = '';
if (await this.pathExists(kiloModesPath)) { if (await this.pathExists(kiloModesPath)) {
existingContent = await this.readFile(kiloModesPath); const existingContent = await this.readFile(kiloModesPath);
// Parse existing modes try {
const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g); config = yaml.parse(existingContent) || {};
for (const match of modeMatches) { } catch {
existingModes.push(match[1]); // If parsing fails, start fresh but warn user
console.log(chalk.yellow('Warning: Could not parse existing .kilocodemodes, starting fresh'));
config = {};
} }
console.log(chalk.yellow(`Found existing .kilocodemodes file with ${existingModes.length} modes`)); }
// Ensure customModes array exists
if (!Array.isArray(config.customModes)) {
config.customModes = [];
} }
// Generate agent launchers // Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
// Create modes content // Create mode objects and add to config
let newModesContent = '';
let addedCount = 0; let addedCount = 0;
let skippedCount = 0;
for (const artifact of agentArtifacts) { for (const artifact of agentArtifacts) {
const slug = `bmad-${artifact.module}-${artifact.name}`; const modeObject = await this.createModeObject(artifact, projectDir);
config.customModes.push(modeObject);
// Skip if already exists
if (existingModes.includes(slug)) {
console.log(chalk.dim(` Skipping ${slug} - already exists`));
skippedCount++;
continue;
}
const modeEntry = await this.createModeEntry(artifact, projectDir);
newModesContent += modeEntry;
addedCount++; addedCount++;
} }
// Build final content // Write .kilocodemodes file with proper YAML structure
let finalContent = ''; const finalContent = yaml.stringify(config, { lineWidth: 0 });
if (existingContent) {
finalContent = existingContent.trim() + '\n' + newModesContent;
} else {
finalContent = 'customModes:\n' + newModesContent;
}
// Write .kilocodemodes file
await this.writeFile(kiloModesPath, finalContent); await this.writeFile(kiloModesPath, finalContent);
// Generate workflow commands
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
// Write to .kilocode/workflows/ directory
const workflowsDir = path.join(projectDir, '.kilocode', 'workflows');
await this.ensureDir(workflowsDir);
// Clear old BMAD workflows before writing new ones
await this.clearBmadWorkflows(workflowsDir);
// Write workflow files
const workflowCount = await workflowGenerator.writeDashArtifacts(workflowsDir, workflowArtifacts);
// Generate task and tool commands
const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
// Write task/tool files to workflows directory (same location as workflows)
await taskToolGen.writeDashArtifacts(workflowsDir, taskToolArtifacts);
const taskCount = taskToolCounts.tasks || 0;
const toolCount = taskToolCounts.tools || 0;
console.log(chalk.green(`${this.name} configured:`)); console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${addedCount} modes added`)); console.log(chalk.dim(` - ${addedCount} modes added`));
if (skippedCount > 0) { console.log(chalk.dim(` - ${workflowCount} workflows exported`));
console.log(chalk.dim(` - ${skippedCount} modes skipped (already exist)`)); console.log(chalk.dim(` - ${taskCount} tasks exported`));
} console.log(chalk.dim(` - ${toolCount} tools exported`));
console.log(chalk.dim(` - Configuration file: ${this.configFile}`)); console.log(chalk.dim(` - Configuration file: ${this.configFile}`));
console.log(chalk.dim(` - Workflows directory: .kilocode/workflows/`));
console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode')); console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode'));
return { return {
success: true, success: true,
modes: addedCount, modes: addedCount,
skipped: skippedCount, workflows: workflowCount,
tasks: taskCount,
tools: toolCount,
}; };
} }
/** /**
* Create a mode entry for an agent * Create a mode object for an agent
* @param {Object} artifact - Agent artifact
* @param {string} projectDir - Project directory
* @returns {Object} Mode object for YAML serialization
*/ */
async createModeEntry(artifact, projectDir) { async createModeObject(artifact, projectDir) {
// Extract metadata from launcher content // Extract metadata from launcher content
const titleMatch = artifact.content.match(/title="([^"]+)"/); const titleMatch = artifact.content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name); const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name);
@ -102,8 +123,8 @@ class KiloSetup extends BaseIdeSetup {
const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/); const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/);
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
// Get the activation header from central template // Get the activation header from central template (trim to avoid YAML formatting issues)
const activationHeader = await this.getAgentCommandHeader(); const activationHeader = (await this.getAgentCommandHeader()).trim();
const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/); const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/);
const roleDefinition = roleDefinitionMatch const roleDefinition = roleDefinitionMatch
@ -113,22 +134,15 @@ class KiloSetup extends BaseIdeSetup {
// Get relative path // Get relative path
const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/'); const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/');
// Build mode entry (KiloCode uses same schema as Roo) // Build mode object (KiloCode uses same schema as Roo)
const slug = `bmad-${artifact.module}-${artifact.name}`; return {
let modeEntry = ` - slug: ${slug}\n`; slug: `bmad-${artifact.module}-${artifact.name}`,
modeEntry += ` name: '${icon} ${title}'\n`; name: `${icon} ${title}`,
modeEntry += ` roleDefinition: ${roleDefinition}\n`; roleDefinition: roleDefinition,
modeEntry += ` whenToUse: ${whenToUse}\n`; whenToUse: whenToUse,
modeEntry += ` customInstructions: |\n`; customInstructions: `${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`,
modeEntry += ` ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`; groups: ['read', 'edit', 'browser', 'command', 'mcp'],
modeEntry += ` groups:\n`; };
modeEntry += ` - read\n`;
modeEntry += ` - edit\n`;
modeEntry += ` - browser\n`;
modeEntry += ` - command\n`;
modeEntry += ` - mcp\n`;
return modeEntry;
} }
/** /**
@ -141,6 +155,22 @@ class KiloSetup extends BaseIdeSetup {
.join(' '); .join(' ');
} }
/**
* Clear old BMAD workflow files from workflows directory
* @param {string} workflowsDir - Workflows directory path
*/
async clearBmadWorkflows(workflowsDir) {
const fs = require('fs-extra');
if (!(await fs.pathExists(workflowsDir))) return;
const entries = await fs.readdir(workflowsDir);
for (const entry of entries) {
if (entry.startsWith('bmad-') && entry.endsWith('.md')) {
await fs.remove(path.join(workflowsDir, entry));
}
}
}
/** /**
* Cleanup KiloCode configuration * Cleanup KiloCode configuration
*/ */
@ -151,28 +181,29 @@ class KiloSetup extends BaseIdeSetup {
if (await fs.pathExists(kiloModesPath)) { if (await fs.pathExists(kiloModesPath)) {
const content = await fs.readFile(kiloModesPath, 'utf8'); const content = await fs.readFile(kiloModesPath, 'utf8');
// Remove BMAD modes only try {
const lines = content.split('\n'); const config = yaml.parse(content) || {};
const filteredLines = [];
let skipMode = false;
let removedCount = 0;
for (const line of lines) { if (Array.isArray(config.customModes)) {
if (/^\s*- slug: bmad-/.test(line)) { const originalCount = config.customModes.length;
skipMode = true; // Remove BMAD modes only (keep non-BMAD modes)
removedCount++; config.customModes = config.customModes.filter((mode) => !mode.slug || !mode.slug.startsWith('bmad-'));
} else if (skipMode && /^\s*- slug: /.test(line)) { const removedCount = originalCount - config.customModes.length;
skipMode = false;
}
if (!skipMode) { if (removedCount > 0) {
filteredLines.push(line); await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 }));
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`));
}
} }
} catch {
// If parsing fails, leave file as-is
console.log(chalk.yellow('Warning: Could not parse .kilocodemodes for cleanup'));
} }
await fs.writeFile(kiloModesPath, filteredLines.join('\n'));
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`));
} }
// Clean up workflow files
const workflowsDir = path.join(projectDir, '.kilocode', 'workflows');
await this.clearBmadWorkflows(workflowsDir);
} }
/** /**
@ -185,31 +216,28 @@ class KiloSetup extends BaseIdeSetup {
*/ */
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const kilocodemodesPath = path.join(projectDir, this.configFile); const kilocodemodesPath = path.join(projectDir, this.configFile);
let existingContent = ''; let config = {};
// Read existing .kilocodemodes file // Read existing .kilocodemodes file
if (await this.pathExists(kilocodemodesPath)) { if (await this.pathExists(kilocodemodesPath)) {
existingContent = await this.readFile(kilocodemodesPath); const existingContent = await this.readFile(kilocodemodesPath);
try {
config = yaml.parse(existingContent) || {};
} catch {
config = {};
}
} }
// Create custom agent mode entry // Ensure customModes array exists
if (!Array.isArray(config.customModes)) {
config.customModes = [];
}
// Create custom agent mode object
const slug = `bmad-custom-${agentName.toLowerCase()}`; const slug = `bmad-custom-${agentName.toLowerCase()}`;
const modeEntry = ` - slug: ${slug}
name: 'BMAD Custom: ${agentName}'
description: |
Custom BMAD agent: ${agentName}
** IMPORTANT**: Run @${agentPath} first to load the complete agent!
This is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.
prompt: |
@${agentPath}
always: false
permissions: all
`;
// Check if mode already exists // Check if mode already exists
if (existingContent.includes(slug)) { if (config.customModes.some((mode) => mode.slug === slug)) {
return { return {
ide: 'kilo', ide: 'kilo',
path: this.configFile, path: this.configFile,
@ -219,24 +247,18 @@ class KiloSetup extends BaseIdeSetup {
}; };
} }
// Build final content // Add custom mode object
let finalContent = ''; config.customModes.push({
if (existingContent) { slug: slug,
// Find customModes section or add it name: `BMAD Custom: ${agentName}`,
if (existingContent.includes('customModes:')) { description: `Custom BMAD agent: ${agentName}\n\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\nThis is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.\n`,
// Append to existing customModes prompt: `@${agentPath}\n`,
finalContent = existingContent + modeEntry; always: false,
} else { permissions: 'all',
// Add customModes section });
finalContent = existingContent.trim() + '\n\ncustomModes:\n' + modeEntry;
}
} else {
// Create new .kilocodemodes file with customModes
finalContent = 'customModes:\n' + modeEntry;
}
// Write .kilocodemodes file // Write .kilocodemodes file with proper YAML structure
await this.writeFile(kilocodemodesPath, finalContent); await this.writeFile(kilocodemodesPath, yaml.stringify(config, { lineWidth: 0 }));
return { return {
ide: 'kilo', ide: 'kilo',

View File

@ -124,8 +124,13 @@ platforms:
category: ide category: ide
description: "OpenCode terminal coding assistant" description: "OpenCode terminal coding assistant"
installer: installer:
target_dir: .opencode/command targets:
template_type: opencode - target_dir: .opencode/agent
template_type: opencode
artifact_types: [agents]
- target_dir: .opencode/command
template_type: opencode
artifact_types: [workflows, tasks, tools]
qwen: qwen:
name: "QwenCoder" name: "QwenCoder"

View File

@ -417,7 +417,7 @@ class ModuleManager {
if (needsDependencyInstall || wasNewClone || nodeModulesMissing) { if (needsDependencyInstall || wasNewClone || nodeModulesMissing) {
const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start(); const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
try { try {
execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress --legacy-peer-deps', { execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
cwd: moduleCacheDir, cwd: moduleCacheDir,
stdio: 'pipe', stdio: 'pipe',
timeout: 120_000, // 2 minute timeout timeout: 120_000, // 2 minute timeout
@ -442,7 +442,7 @@ class ModuleManager {
if (packageJsonNewer) { if (packageJsonNewer) {
const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start(); const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
try { try {
execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress --legacy-peer-deps', { execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
cwd: moduleCacheDir, cwd: moduleCacheDir,
stdio: 'pipe', stdio: 'pipe',
timeout: 120_000, // 2 minute timeout timeout: 120_000, // 2 minute timeout

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