Compare commits
4 Commits
852d9d8445
...
7d705aa08b
| Author | SHA1 | Date |
|---|---|---|
|
|
7d705aa08b | |
|
|
23f650ff4d | |
|
|
52bd250a8c | |
|
|
b6ade22984 |
61
README.md
61
README.md
|
|
@ -79,6 +79,8 @@ With **BMad Builder**, you can architect both simple agents and vastly complex d
|
|||
|
||||
### 1. Install BMad Method
|
||||
|
||||
#### Interactive Installation (Default)
|
||||
|
||||
```bash
|
||||
# Install v6 Alpha (recommended)
|
||||
npx bmad-method@alpha install
|
||||
|
|
@ -87,6 +89,65 @@ npx bmad-method@alpha install
|
|||
npx bmad-method install
|
||||
```
|
||||
|
||||
#### Non-Interactive Installation (CI/CD, Automation)
|
||||
|
||||
For automated deployments and CI/CD pipelines:
|
||||
|
||||
```bash
|
||||
# Minimal installation with defaults
|
||||
npx bmad-method@alpha install -y
|
||||
|
||||
# Custom configuration
|
||||
npx bmad-method@alpha install -y \
|
||||
--user-name=Alice \
|
||||
--skill-level=advanced \
|
||||
--output-folder=.bmad-output
|
||||
|
||||
# Team-based installation (fullstack team)
|
||||
npx bmad-method@alpha install -y --team=fullstack
|
||||
|
||||
# Team with modifications
|
||||
npx bmad-method@alpha install -y --team=fullstack --agents=+dev
|
||||
|
||||
# Selective agents and workflows
|
||||
npx bmad-method@alpha install -y \
|
||||
--agents=dev,architect,pm \
|
||||
--workflows=create-prd,create-tech-spec,dev-story
|
||||
|
||||
# Profile-based installation
|
||||
npx bmad-method@alpha install -y --profile=minimal
|
||||
npx bmad-method@alpha install -y --profile=solo-dev
|
||||
```
|
||||
|
||||
**Available Options:**
|
||||
|
||||
- `-y, --non-interactive` - Skip all prompts, use defaults
|
||||
- `--user-name <name>` - User name for configuration
|
||||
- `--skill-level <level>` - beginner, intermediate, advanced
|
||||
- `--output-folder <path>` - Output folder for BMAD artifacts
|
||||
- `--modules <list>` - Comma-separated module list
|
||||
- `--agents <list>` - Comma-separated agent list or 'all', 'none'
|
||||
- `--workflows <list>` - Comma-separated workflow list or 'all', 'none'
|
||||
- `--team <name>` - Install predefined team (fullstack, gamedev)
|
||||
- `--profile <name>` - Installation profile (minimal, full, solo-dev, team)
|
||||
|
||||
**Modifiers:**
|
||||
|
||||
- `--agents=+dev` - Add agent to team/profile selection
|
||||
- `--agents=-dev` - Remove agent from team/profile selection
|
||||
|
||||
**Available Teams:**
|
||||
|
||||
- `fullstack` - analyst, architect, pm, sm, ux-designer
|
||||
- `gamedev` - game-designer, game-dev, game-architect, game-scrum-master
|
||||
|
||||
**Available Profiles:**
|
||||
|
||||
- `minimal` - Core + dev agent + essential workflows
|
||||
- `full` - Everything (all modules, agents, workflows)
|
||||
- `solo-dev` - Single developer setup
|
||||
- `team` - Team collaboration setup
|
||||
|
||||
### 2. Initialize Your Project
|
||||
|
||||
Load any agent in your IDE and run:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,440 @@
|
|||
# Non-Interactive Installation Guide
|
||||
|
||||
This guide helps you convert interactive BMAD installations to non-interactive CLI commands for automation, CI/CD pipelines, and scripted deployments.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [Migration from Interactive to CLI](#migration-from-interactive-to-cli)
|
||||
- [Common Use Cases](#common-use-cases)
|
||||
- [CLI Options Reference](#cli-options-reference)
|
||||
- [Team-Based Installation](#team-based-installation)
|
||||
- [Profile-Based Installation](#profile-based-installation)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Minimal Non-Interactive Installation
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y
|
||||
```
|
||||
|
||||
This installs BMAD with:
|
||||
- Default user name from system (USER environment variable)
|
||||
- Intermediate skill level
|
||||
- Default output folder (`_bmad-output`)
|
||||
- BMM module
|
||||
- All agents and workflows from BMM
|
||||
|
||||
### Custom Non-Interactive Installation
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y \
|
||||
--user-name=YourName \
|
||||
--skill-level=advanced \
|
||||
--output-folder=.artifacts
|
||||
```
|
||||
|
||||
## Migration from Interactive to CLI
|
||||
|
||||
### Step 1: Note Your Current Configuration
|
||||
|
||||
If you have an existing BMAD installation, check your configuration:
|
||||
|
||||
```bash
|
||||
# View your current configuration
|
||||
cat _bmad/core/config.yaml
|
||||
cat _bmad/bmm/config.yaml
|
||||
```
|
||||
|
||||
Example output:
|
||||
```yaml
|
||||
user_name: Alice
|
||||
user_skill_level: intermediate
|
||||
output_folder: "{project-root}/_bmad-output"
|
||||
communication_language: English
|
||||
```
|
||||
|
||||
### Step 2: Convert to CLI Command
|
||||
|
||||
Based on your configuration, build the equivalent CLI command:
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y \
|
||||
--user-name=Alice \
|
||||
--skill-level=intermediate \
|
||||
--output-folder=_bmad-output \
|
||||
--communication-language=English
|
||||
```
|
||||
|
||||
### Step 3: Replicate Module and Agent Selection
|
||||
|
||||
Check what agents and workflows you currently have:
|
||||
|
||||
```bash
|
||||
# View installed agents
|
||||
cat _bmad/_config/agents.csv
|
||||
|
||||
# View installed workflows
|
||||
cat _bmad/_config/workflows.csv
|
||||
```
|
||||
|
||||
If you have specific agents installed, add them to your command:
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y \
|
||||
--user-name=Alice \
|
||||
--agents=dev,architect,pm
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. CI/CD Pipeline Installation
|
||||
|
||||
```yaml
|
||||
# .github/workflows/setup-bmad.yml
|
||||
name: Setup BMAD
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Install BMAD
|
||||
run: npx bmad-method@alpha install -y --profile=minimal
|
||||
```
|
||||
|
||||
### 2. Docker Container Setup
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Install BMAD non-interactively
|
||||
RUN npx bmad-method@alpha install -y \
|
||||
--user-name=ContainerUser \
|
||||
--skill-level=intermediate \
|
||||
--output-folder=.bmad-output
|
||||
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
### 3. Team Onboarding Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# onboard-developer.sh
|
||||
|
||||
echo "Setting up BMAD for $USER..."
|
||||
|
||||
npx bmad-method@alpha install -y \
|
||||
--user-name=$USER \
|
||||
--skill-level=intermediate \
|
||||
--team=fullstack \
|
||||
--output-folder=.bmad-output
|
||||
|
||||
echo "BMAD installation complete!"
|
||||
```
|
||||
|
||||
### 4. Infrastructure as Code
|
||||
|
||||
```bash
|
||||
# terraform/setup.sh
|
||||
npx bmad-method@alpha install -y \
|
||||
--user-name=TerraformBot \
|
||||
--skill-level=advanced \
|
||||
--modules=core,bmm \
|
||||
--agents=dev,architect,analyst \
|
||||
--workflows=create-prd,create-architecture,dev-story
|
||||
```
|
||||
|
||||
### 5. Minimal Developer Setup
|
||||
|
||||
For developers who only need code generation:
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y \
|
||||
--profile=minimal \
|
||||
--user-name=$USER
|
||||
```
|
||||
|
||||
## CLI Options Reference
|
||||
|
||||
### Core Options
|
||||
|
||||
| Option | Description | Default | Example |
|
||||
|--------|-------------|---------|---------|
|
||||
| `-y, --non-interactive` | Skip all prompts | `false` | `install -y` |
|
||||
| `--user-name <name>` | User name | System user | `--user-name=Alice` |
|
||||
| `--skill-level <level>` | Skill level | `intermediate` | `--skill-level=advanced` |
|
||||
| `--output-folder <path>` | Output folder | `_bmad-output` | `--output-folder=.artifacts` |
|
||||
| `--communication-language <lang>` | Communication language | `English` | `--communication-language=Spanish` |
|
||||
| `--document-language <lang>` | Document language | `English` | `--document-language=French` |
|
||||
|
||||
### Module & Selection Options
|
||||
|
||||
| Option | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `--modules <list>` | Comma-separated modules | `--modules=core,bmm,bmbb` |
|
||||
| `--agents <list>` | Comma-separated agents | `--agents=dev,architect,pm` |
|
||||
| `--workflows <list>` | Comma-separated workflows | `--workflows=create-prd,dev-story` |
|
||||
|
||||
### Team & Profile Options
|
||||
|
||||
| Option | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `--team <name>` | Install predefined team | `--team=fullstack` |
|
||||
| `--profile <name>` | Installation profile | `--profile=minimal` |
|
||||
|
||||
## Team-Based Installation
|
||||
|
||||
Teams are predefined bundles of agents and workflows optimized for specific use cases.
|
||||
|
||||
### Available Teams
|
||||
|
||||
#### Fullstack Team
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y --team=fullstack
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Agents: analyst, architect, pm, sm, ux-designer
|
||||
- Module: BMM
|
||||
|
||||
**Use for:** Full product development teams
|
||||
|
||||
#### Game Development Team
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y --team=gamedev
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Agents: game-designer, game-dev, game-architect, game-scrum-master
|
||||
- Workflows: brainstorm-game, game-brief, gdd, narrative
|
||||
- Module: BMGD (Game Development)
|
||||
|
||||
**Use for:** Game development projects
|
||||
|
||||
### Modifying Team Selections
|
||||
|
||||
You can add or remove agents from a team:
|
||||
|
||||
```bash
|
||||
# Add dev agent to fullstack team
|
||||
npx bmad-method@alpha install -y --team=fullstack --agents=+dev
|
||||
|
||||
# Remove ux-designer from fullstack team
|
||||
npx bmad-method@alpha install -y --team=fullstack --agents=-ux-designer
|
||||
|
||||
# Add and remove multiple
|
||||
npx bmad-method@alpha install -y --team=fullstack --agents=+dev,+tea,-ux-designer
|
||||
```
|
||||
|
||||
## Profile-Based Installation
|
||||
|
||||
Profiles are pre-configured installations for common scenarios.
|
||||
|
||||
### Available Profiles
|
||||
|
||||
#### Minimal Profile
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y --profile=minimal
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Modules: core
|
||||
- Agents: dev
|
||||
- Workflows: create-tech-spec, quick-dev
|
||||
|
||||
**Use for:** Simple development, code generation only
|
||||
|
||||
#### Solo Developer Profile
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y --profile=solo-dev
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Modules: core, bmm
|
||||
- Agents: dev, architect, analyst, tech-writer
|
||||
- Workflows: create-tech-spec, quick-dev, dev-story, code-review, create-prd, create-architecture
|
||||
|
||||
**Use for:** Individual developers working on full projects
|
||||
|
||||
#### Full Profile
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y --profile=full
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- All modules
|
||||
- All agents
|
||||
- All workflows
|
||||
|
||||
**Use for:** Maximum flexibility, exploring all BMAD features
|
||||
|
||||
#### Team Profile
|
||||
|
||||
```bash
|
||||
npx bmad-method@alpha install -y --profile=team
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Modules: core, bmm
|
||||
- Agents: dev, architect, pm, sm, analyst, ux-designer
|
||||
- Workflows: create-product-brief, create-prd, create-architecture, create-epics-and-stories, sprint-planning, create-story, dev-story, code-review, workflow-init
|
||||
|
||||
**Use for:** Team collaboration, full agile workflow
|
||||
|
||||
### Overriding Profile Settings
|
||||
|
||||
```bash
|
||||
# Use minimal profile but add architect agent
|
||||
npx bmad-method@alpha install -y --profile=minimal --agents=dev,architect
|
||||
|
||||
# Use solo-dev profile with custom output folder
|
||||
npx bmad-method@alpha install -y --profile=solo-dev --output-folder=.custom
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "Team not found"
|
||||
|
||||
**Solution:** Check available teams:
|
||||
|
||||
```bash
|
||||
# List available teams in your installation
|
||||
ls src/modules/*/teams/team-*.yaml
|
||||
```
|
||||
|
||||
Available teams depend on installed modules. Ensure you have the required modules.
|
||||
|
||||
### Issue: "Agent not found in manifest"
|
||||
|
||||
**Solution:** The agent name might be incorrect. Check available agents:
|
||||
|
||||
```bash
|
||||
# View all available agents
|
||||
find src/modules -name "*.agent.yaml" -o -name "*-agent.md"
|
||||
```
|
||||
|
||||
Common agent names: `dev`, `architect`, `pm`, `sm`, `analyst`, `ux-designer`, `tech-writer`
|
||||
|
||||
### Issue: "Installation hangs"
|
||||
|
||||
**Solution:** Ensure you're using the `-y` flag for non-interactive mode:
|
||||
|
||||
```bash
|
||||
# Correct
|
||||
npx bmad-method@alpha install -y
|
||||
|
||||
# Incorrect (will wait for input)
|
||||
npx bmad-method@alpha install
|
||||
```
|
||||
|
||||
### Issue: "Permission denied"
|
||||
|
||||
**Solution:** Check file permissions or run with appropriate privileges:
|
||||
|
||||
```bash
|
||||
# Check current directory permissions
|
||||
ls -la
|
||||
|
||||
# Ensure you have write permissions
|
||||
chmod u+w .
|
||||
```
|
||||
|
||||
### Issue: "Invalid skill level"
|
||||
|
||||
**Solution:** Use one of the valid skill levels:
|
||||
|
||||
- `beginner`
|
||||
- `intermediate`
|
||||
- `advanced`
|
||||
|
||||
```bash
|
||||
# Correct
|
||||
npx bmad-method@alpha install -y --skill-level=advanced
|
||||
|
||||
# Incorrect
|
||||
npx bmad-method@alpha install -y --skill-level=expert
|
||||
```
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
### Reproducible Installation
|
||||
|
||||
Save your installation command for reproducibility:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# install-bmad.sh - Reproducible BMAD installation
|
||||
|
||||
npx bmad-method@alpha install -y \
|
||||
--user-name=ProjectBot \
|
||||
--skill-level=intermediate \
|
||||
--output-folder=_bmad-output \
|
||||
--modules=core,bmm \
|
||||
--agents=dev,architect,pm,analyst \
|
||||
--workflows=create-prd,create-architecture,create-tech-spec,dev-story,code-review \
|
||||
--communication-language=English \
|
||||
--document-language=English
|
||||
```
|
||||
|
||||
### Environment-Based Installation
|
||||
|
||||
Use environment variables for flexibility:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Detect user from environment
|
||||
USER_NAME=${BMAD_USER:-$USER}
|
||||
|
||||
# Detect skill level from environment or default to intermediate
|
||||
SKILL_LEVEL=${BMAD_SKILL_LEVEL:-intermediate}
|
||||
|
||||
npx bmad-method@alpha install -y \
|
||||
--user-name=$USER_NAME \
|
||||
--skill-level=$SKILL_LEVEL \
|
||||
--output-folder=${BMAD_OUTPUT_FOLDER:-_bmad-output}
|
||||
```
|
||||
|
||||
### Conditional Installation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Install different configurations based on environment
|
||||
|
||||
if [ "$CI" = "true" ]; then
|
||||
# CI environment: minimal installation
|
||||
npx bmad-method@alpha install -y --profile=minimal
|
||||
elif [ "$TEAM_MODE" = "true" ]; then
|
||||
# Team development: full team setup
|
||||
npx bmad-method@alpha install -y --team=fullstack
|
||||
else
|
||||
# Local development: solo-dev profile
|
||||
npx bmad-method@alpha install -y --profile=solo-dev --user-name=$USER
|
||||
fi
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read the [main README](../README.md) for BMAD overview
|
||||
- Explore [Custom Content Installation](./custom-content-installation.md)
|
||||
- Join the [BMAD Discord](https://discord.gg/gk8jAdXWmj) community
|
||||
|
||||
## Feedback
|
||||
|
||||
Found an issue or have a suggestion? Please report it at:
|
||||
https://github.com/bmad-code-org/BMAD-METHOD/issues
|
||||
|
|
@ -8,11 +8,44 @@ const ui = new UI();
|
|||
|
||||
module.exports = {
|
||||
command: 'install',
|
||||
description: 'Install BMAD Core agents and tools',
|
||||
options: [],
|
||||
description: `Install BMAD Core agents and tools
|
||||
|
||||
Examples:
|
||||
bmad install # Interactive installation
|
||||
bmad install -y # Non-interactive with defaults
|
||||
bmad install -y --user-name=Alice --skill-level=advanced
|
||||
bmad install -y --team=fullstack # Install fullstack team
|
||||
bmad install -y --team=fullstack --agents=+dev # Add dev to fullstack team
|
||||
bmad install -y --agents=dev,architect,pm # Selective agents
|
||||
bmad install -y --profile=minimal # Minimal profile
|
||||
bmad install -y --workflows=create-prd,dev-story # Selective workflows
|
||||
|
||||
Special Values:
|
||||
--agents=all, --agents=none, --agents=minimal
|
||||
--workflows=all, --workflows=none, --workflows=minimal
|
||||
|
||||
Modifiers:
|
||||
--agents=+dev Add agent to team/profile selection
|
||||
--agents=-dev Remove agent from team/profile selection
|
||||
|
||||
Available Teams: fullstack, gamedev
|
||||
Available Profiles: minimal, full, solo-dev, team`,
|
||||
options: [
|
||||
['-y, --non-interactive', 'Run without prompts, use defaults'],
|
||||
['--user-name <name>', 'User name for configuration'],
|
||||
['--skill-level <level>', 'User skill level (beginner, intermediate, advanced)'],
|
||||
['--output-folder <path>', 'Output folder path for BMAD artifacts'],
|
||||
['--modules <list>', 'Comma-separated list of modules to install (e.g., core,bmm)'],
|
||||
['--agents <list>', 'Comma-separated list of agents to install (e.g., dev,architect,pm)'],
|
||||
['--workflows <list>', 'Comma-separated list of workflows to install'],
|
||||
['--team <name>', 'Install predefined team bundle (e.g., fullstack, gamedev)'],
|
||||
['--profile <name>', 'Installation profile (minimal, full, solo-dev)'],
|
||||
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
||||
['--document-language <lang>', 'Language for generated documents (default: English)'],
|
||||
],
|
||||
action: async (options) => {
|
||||
try {
|
||||
const config = await ui.promptInstall();
|
||||
const config = await ui.promptInstall(options);
|
||||
|
||||
// Handle cancel
|
||||
if (config.actionType === 'cancel') {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const chalk = require('chalk');
|
|||
const inquirer = require('inquirer');
|
||||
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
|
||||
const { CLIUtils } = require('../../../lib/cli-utils');
|
||||
const { getEnvironmentDefaults, resolveValue } = require('./env-resolver');
|
||||
|
||||
class ConfigCollector {
|
||||
constructor() {
|
||||
|
|
@ -792,6 +793,180 @@ class ConfigCollector {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect configuration for a module in non-interactive mode
|
||||
* @param {string} moduleName - Module name
|
||||
* @param {string} projectDir - Target project directory
|
||||
* @param {Object} cliOptions - CLI options passed by user
|
||||
* @returns {Object} Collected config for the module
|
||||
*/
|
||||
async collectModuleConfigNonInteractive(moduleName, projectDir, cliOptions = {}) {
|
||||
this.currentProjectDir = projectDir;
|
||||
|
||||
// Load existing config if not already loaded
|
||||
if (!this.existingConfig) {
|
||||
await this.loadExistingConfig(projectDir);
|
||||
}
|
||||
|
||||
// Initialize allAnswers if not already initialized
|
||||
if (!this.allAnswers) {
|
||||
this.allAnswers = {};
|
||||
}
|
||||
|
||||
// Get environment defaults
|
||||
const envDefaults = getEnvironmentDefaults();
|
||||
|
||||
// Try to load module config schema
|
||||
let installerConfigPath = null;
|
||||
let moduleConfigPath = null;
|
||||
|
||||
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
|
||||
const customPath = this.customModulePaths.get(moduleName);
|
||||
installerConfigPath = path.join(customPath, '_module-installer', 'module.yaml');
|
||||
moduleConfigPath = path.join(customPath, 'module.yaml');
|
||||
} else {
|
||||
installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
|
||||
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||
}
|
||||
|
||||
// If not found, try to find via module manager
|
||||
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
const moduleManager = new ModuleManager();
|
||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||
|
||||
if (moduleSourcePath) {
|
||||
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
|
||||
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||
}
|
||||
}
|
||||
|
||||
let configPath = null;
|
||||
if (await fs.pathExists(moduleConfigPath)) {
|
||||
configPath = moduleConfigPath;
|
||||
} else if (await fs.pathExists(installerConfigPath)) {
|
||||
configPath = installerConfigPath;
|
||||
} else {
|
||||
// No config for this module - use existing if available
|
||||
if (this.existingConfig && this.existingConfig[moduleName]) {
|
||||
this.collectedConfig[moduleName] = { ...this.existingConfig[moduleName] };
|
||||
} else {
|
||||
this.collectedConfig[moduleName] = {};
|
||||
}
|
||||
return this.collectedConfig[moduleName];
|
||||
}
|
||||
|
||||
const configContent = await fs.readFile(configPath, 'utf8');
|
||||
const moduleConfig = yaml.parse(configContent);
|
||||
|
||||
if (!moduleConfig) {
|
||||
this.collectedConfig[moduleName] = {};
|
||||
return this.collectedConfig[moduleName];
|
||||
}
|
||||
|
||||
// Initialize module config
|
||||
if (!this.collectedConfig[moduleName]) {
|
||||
this.collectedConfig[moduleName] = {};
|
||||
}
|
||||
|
||||
// Process each config item
|
||||
const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
|
||||
|
||||
for (const key of configKeys) {
|
||||
const item = moduleConfig[key];
|
||||
|
||||
// Skip if not a config object
|
||||
if (!item || typeof item !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = null;
|
||||
|
||||
// Resolution order: CLI → ENV → existing → default → hardcoded
|
||||
if (moduleName === 'core') {
|
||||
// Core module has special mappings
|
||||
if (key === 'user_name') {
|
||||
value = resolveValue(cliOptions.userName, null, envDefaults.userName);
|
||||
} else if (key === 'user_skill_level') {
|
||||
value = resolveValue(cliOptions.skillLevel, null, item.default || 'intermediate');
|
||||
} else if (key === 'communication_language') {
|
||||
value = resolveValue(
|
||||
cliOptions.communicationLanguage,
|
||||
null,
|
||||
envDefaults.communicationLanguage,
|
||||
);
|
||||
} else if (key === 'document_output_language') {
|
||||
value = resolveValue(cliOptions.documentLanguage, null, envDefaults.documentLanguage);
|
||||
} else if (key === 'output_folder') {
|
||||
value = resolveValue(cliOptions.outputFolder, null, item.default);
|
||||
} else if (item.default !== undefined) {
|
||||
value = item.default;
|
||||
}
|
||||
} else {
|
||||
// For other modules, use defaults
|
||||
if (item.default !== undefined) {
|
||||
value = item.default;
|
||||
} else if (this.existingConfig && this.existingConfig[moduleName]) {
|
||||
value = this.existingConfig[moduleName][key];
|
||||
}
|
||||
}
|
||||
|
||||
// Process result template if present
|
||||
let result;
|
||||
if (item.result && typeof item.result === 'string') {
|
||||
result = item.result;
|
||||
if (typeof value === 'string') {
|
||||
result = result.replace('{value}', value);
|
||||
} else if (value !== undefined && value !== null) {
|
||||
result = result.replace('{value}', String(value));
|
||||
}
|
||||
|
||||
// Replace references to other config values
|
||||
result = result.replaceAll(/{([^}]+)}/g, (match, configKey) => {
|
||||
if (configKey === 'project-root' || configKey === 'value') {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Look in collected config
|
||||
let configValue = this.collectedConfig[moduleName]?.[configKey];
|
||||
if (!configValue) {
|
||||
for (const mod of Object.keys(this.collectedConfig)) {
|
||||
if (mod !== '_meta' && this.collectedConfig[mod]?.[configKey]) {
|
||||
configValue = this.collectedConfig[mod][configKey];
|
||||
if (typeof configValue === 'string' && configValue.includes('{project-root}/')) {
|
||||
configValue = configValue.replace('{project-root}/', '');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configValue || match;
|
||||
});
|
||||
} else if (item.result) {
|
||||
result = value;
|
||||
} else {
|
||||
result = value;
|
||||
}
|
||||
|
||||
// Store the result
|
||||
this.collectedConfig[moduleName][key] = result;
|
||||
this.allAnswers[`${moduleName}_${key}`] = result;
|
||||
}
|
||||
|
||||
// Copy existing values for fields not in schema
|
||||
if (this.existingConfig && this.existingConfig[moduleName]) {
|
||||
for (const [key, value] of Object.entries(this.existingConfig[moduleName])) {
|
||||
if (this.collectedConfig[moduleName][key] === undefined) {
|
||||
this.collectedConfig[moduleName][key] = value;
|
||||
this.allAnswers[`${moduleName}_${key}`] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.collectedConfig[moduleName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace placeholders in a string with collected config values
|
||||
* @param {string} str - String with placeholders
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
const os = require('node:os');
|
||||
|
||||
/**
|
||||
* Environment Variable Resolver
|
||||
*
|
||||
* Resolves configuration values from environment variables
|
||||
* with fallbacks to system defaults.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get user name from environment variables
|
||||
* Tries USER, USERNAME, LOGNAME in order, falls back to system username or 'User'
|
||||
* @returns {string} User name
|
||||
*/
|
||||
function getUserName() {
|
||||
// Try common environment variables
|
||||
const envUser = process.env.USER || process.env.USERNAME || process.env.LOGNAME;
|
||||
|
||||
if (envUser) {
|
||||
return envUser;
|
||||
}
|
||||
|
||||
// Try Node.js os.userInfo()
|
||||
try {
|
||||
const userInfo = os.userInfo();
|
||||
if (userInfo.username) {
|
||||
return userInfo.username;
|
||||
}
|
||||
} catch (error) {
|
||||
// os.userInfo() can fail in some environments
|
||||
}
|
||||
|
||||
// Final fallback
|
||||
return 'User';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system language from environment variables
|
||||
* Tries LANG, LC_ALL, falls back to 'English'
|
||||
* @returns {string} Language name
|
||||
*/
|
||||
function getSystemLanguage() {
|
||||
const lang = process.env.LANG || process.env.LC_ALL;
|
||||
|
||||
if (!lang) {
|
||||
return 'English';
|
||||
}
|
||||
|
||||
// Parse language from locale string (e.g., 'en_US.UTF-8' -> 'English')
|
||||
const langCode = lang.split('_')[0].toLowerCase();
|
||||
|
||||
// Map common language codes to full names
|
||||
const languageMap = {
|
||||
en: 'English',
|
||||
es: 'Spanish',
|
||||
fr: 'French',
|
||||
de: 'German',
|
||||
it: 'Italian',
|
||||
pt: 'Portuguese',
|
||||
ru: 'Russian',
|
||||
ja: 'Japanese',
|
||||
zh: 'Chinese',
|
||||
ko: 'Korean',
|
||||
ar: 'Arabic',
|
||||
hi: 'Hindi',
|
||||
};
|
||||
|
||||
return languageMap[langCode] || 'English';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get home directory from environment
|
||||
* @returns {string} Home directory path
|
||||
*/
|
||||
function getHomeDirectory() {
|
||||
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a config value with priority: CLI > ENV > default
|
||||
* @param {*} cliValue - Value from CLI argument
|
||||
* @param {string} envVar - Environment variable name to check
|
||||
* @param {*} defaultValue - Default value if neither CLI nor ENV is set
|
||||
* @returns {*} Resolved value
|
||||
*/
|
||||
function resolveValue(cliValue, envVar, defaultValue) {
|
||||
// CLI value has highest priority
|
||||
if (cliValue !== undefined && cliValue !== null) {
|
||||
return cliValue;
|
||||
}
|
||||
|
||||
// Try environment variable
|
||||
if (envVar && process.env[envVar]) {
|
||||
return process.env[envVar];
|
||||
}
|
||||
|
||||
// Use default
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all environment-based defaults
|
||||
* @returns {Object} Default config values from environment
|
||||
*/
|
||||
function getEnvironmentDefaults() {
|
||||
return {
|
||||
userName: getUserName(),
|
||||
communicationLanguage: getSystemLanguage(),
|
||||
documentLanguage: getSystemLanguage(),
|
||||
homeDirectory: getHomeDirectory(),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUserName,
|
||||
getSystemLanguage,
|
||||
getHomeDirectory,
|
||||
resolveValue,
|
||||
getEnvironmentDefaults,
|
||||
};
|
||||
|
|
@ -13,12 +13,13 @@ const { XmlHandler } = require('../../../lib/xml-handler');
|
|||
const { DependencyResolver } = require('./dependency-resolver');
|
||||
const { ConfigCollector } = require('./config-collector');
|
||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||
const { AgentPartyGenerator } = require('../../../lib/agent-party-generator');
|
||||
const { CLIUtils } = require('../../../lib/cli-utils');
|
||||
const { ManifestGenerator } = require('./manifest-generator');
|
||||
const { IdeConfigManager } = require('./ide-config-manager');
|
||||
const { CustomHandler } = require('../custom/handler');
|
||||
const { filterCustomizationData } = require('../../../lib/agent/compiler');
|
||||
|
||||
// BMAD installation folder name - this is constant and should never change
|
||||
const BMAD_FOLDER_NAME = '_bmad';
|
||||
|
||||
class Installer {
|
||||
constructor() {
|
||||
|
|
@ -34,60 +35,37 @@ class Installer {
|
|||
this.ideConfigManager = new IdeConfigManager();
|
||||
this.installedFiles = new Set(); // Track all installed files
|
||||
this.ttsInjectedFiles = []; // Track files with TTS injection applied
|
||||
this.bmadFolderName = BMAD_FOLDER_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the bmad installation directory in a project
|
||||
* V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml
|
||||
* Always uses the standard _bmad folder name
|
||||
* Also checks for legacy _cfg folder for migration
|
||||
* @param {string} projectDir - Project directory
|
||||
* @returns {Promise<Object>} { bmadDir: string, hasLegacyCfg: boolean }
|
||||
*/
|
||||
async findBmadDir(projectDir) {
|
||||
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
||||
|
||||
// Check if project directory exists
|
||||
if (!(await fs.pathExists(projectDir))) {
|
||||
// Project doesn't exist yet, return default
|
||||
return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
|
||||
return { bmadDir, hasLegacyCfg: false };
|
||||
}
|
||||
|
||||
let bmadDir = null;
|
||||
// Check for legacy _cfg folder if bmad directory exists
|
||||
let hasLegacyCfg = false;
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(projectDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const bmadPath = path.join(projectDir, entry.name);
|
||||
|
||||
// Check for current _config folder
|
||||
const manifestPath = path.join(bmadPath, '_config', 'manifest.yaml');
|
||||
if (await fs.pathExists(manifestPath)) {
|
||||
// Found a V6+ installation with current _config folder
|
||||
return { bmadDir: bmadPath, hasLegacyCfg: false };
|
||||
}
|
||||
|
||||
// Check for legacy _cfg folder
|
||||
const legacyManifestPath = path.join(bmadPath, '_cfg', 'manifest.yaml');
|
||||
if (await fs.pathExists(legacyManifestPath)) {
|
||||
bmadDir = bmadPath;
|
||||
if (await fs.pathExists(bmadDir)) {
|
||||
const legacyCfgPath = path.join(bmadDir, '_cfg');
|
||||
if (await fs.pathExists(legacyCfgPath)) {
|
||||
hasLegacyCfg = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.log(chalk.red('Error reading project directory for BMAD installation detection'));
|
||||
}
|
||||
|
||||
// If we found a bmad directory (with or without legacy _cfg)
|
||||
if (bmadDir) {
|
||||
return { bmadDir, hasLegacyCfg };
|
||||
}
|
||||
|
||||
// No V6+ installation found, return default
|
||||
// This will be used for new installations
|
||||
return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* @function copyFileWithPlaceholderReplacement
|
||||
* @intent Copy files from BMAD source to installation directory with dynamic content transformation
|
||||
|
|
@ -120,7 +98,7 @@ class Installer {
|
|||
*
|
||||
* 3. Document marker in instructions.md (if applicable)
|
||||
*/
|
||||
async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
|
||||
async copyFileWithPlaceholderReplacement(sourcePath, targetPath) {
|
||||
// List of text file extensions that should have placeholder replacement
|
||||
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
|
||||
const ext = path.extname(sourcePath).toLowerCase();
|
||||
|
|
@ -285,7 +263,7 @@ class Installer {
|
|||
// Check for already configured IDEs
|
||||
const { Detector } = require('./detector');
|
||||
const detector = new Detector();
|
||||
const bmadDir = path.join(projectDir, this.bmadFolderName || 'bmad');
|
||||
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
||||
|
||||
// During full reinstall, use the saved previous IDEs since bmad dir was deleted
|
||||
// Otherwise detect from existing installation
|
||||
|
|
@ -532,18 +510,14 @@ class Installer {
|
|||
}
|
||||
}
|
||||
|
||||
// Always use _bmad as the folder name
|
||||
const bmadFolderName = '_bmad';
|
||||
this.bmadFolderName = bmadFolderName; // Store for use in other methods
|
||||
|
||||
// Store AgentVibes configuration for injection point processing
|
||||
this.enableAgentVibes = config.enableAgentVibes || false;
|
||||
|
||||
// Set bmad folder name on module manager and IDE manager for placeholder replacement
|
||||
this.moduleManager.setBmadFolderName(bmadFolderName);
|
||||
this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
||||
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
|
||||
this.moduleManager.setCustomModulePaths(customModulePaths);
|
||||
this.ideManager.setBmadFolderName(bmadFolderName);
|
||||
this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
||||
|
||||
// Tool selection will be collected after we determine if it's a reinstall/update/new install
|
||||
|
||||
|
|
@ -553,14 +527,8 @@ class Installer {
|
|||
// Resolve target directory (path.resolve handles platform differences)
|
||||
const projectDir = path.resolve(config.directory);
|
||||
|
||||
let existingBmadDir = null;
|
||||
let existingBmadFolderName = null;
|
||||
|
||||
if (await fs.pathExists(projectDir)) {
|
||||
const result = await this.findBmadDir(projectDir);
|
||||
existingBmadDir = result.bmadDir;
|
||||
existingBmadFolderName = path.basename(existingBmadDir);
|
||||
}
|
||||
// Always use the standard _bmad folder name
|
||||
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
||||
|
||||
// Create a project directory if it doesn't exist (user already confirmed)
|
||||
if (!(await fs.pathExists(projectDir))) {
|
||||
|
|
@ -582,8 +550,6 @@ class Installer {
|
|||
}
|
||||
}
|
||||
|
||||
const bmadDir = path.join(projectDir, bmadFolderName);
|
||||
|
||||
// Check existing installation
|
||||
spinner.text = 'Checking for existing installation...';
|
||||
const existingInstall = await this.detector.detect(bmadDir);
|
||||
|
|
@ -1075,6 +1041,9 @@ class Installer {
|
|||
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
|
||||
ides: config.ides || [],
|
||||
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
|
||||
selectedAgents: config.cliOptions?.agents || null,
|
||||
selectedWorkflows: config.cliOptions?.workflows || null,
|
||||
installMode: config.cliOptions ? 'non-interactive' : 'interactive',
|
||||
});
|
||||
|
||||
// Custom modules are now included in the main modules list - no separate tracking needed
|
||||
|
|
@ -1606,7 +1575,7 @@ class Installer {
|
|||
const targetPath = path.join(agentsDir, fileName);
|
||||
|
||||
if (await fs.pathExists(sourcePath)) {
|
||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
||||
this.installedFiles.add(targetPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -1622,7 +1591,7 @@ class Installer {
|
|||
const targetPath = path.join(tasksDir, fileName);
|
||||
|
||||
if (await fs.pathExists(sourcePath)) {
|
||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
||||
this.installedFiles.add(targetPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -1638,7 +1607,7 @@ class Installer {
|
|||
const targetPath = path.join(toolsDir, fileName);
|
||||
|
||||
if (await fs.pathExists(sourcePath)) {
|
||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
||||
this.installedFiles.add(targetPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -1654,7 +1623,7 @@ class Installer {
|
|||
const targetPath = path.join(templatesDir, fileName);
|
||||
|
||||
if (await fs.pathExists(sourcePath)) {
|
||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
||||
this.installedFiles.add(targetPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -1669,7 +1638,7 @@ class Installer {
|
|||
await fs.ensureDir(path.dirname(targetPath));
|
||||
|
||||
if (await fs.pathExists(dataPath)) {
|
||||
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
|
||||
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath);
|
||||
this.installedFiles.add(targetPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -1759,14 +1728,9 @@ class Installer {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if this is a workflow.yaml file
|
||||
if (file.endsWith('workflow.yaml')) {
|
||||
await fs.ensureDir(path.dirname(targetFile));
|
||||
await this.copyWorkflowYamlStripped(sourceFile, targetFile);
|
||||
} else {
|
||||
// Copy the file with placeholder replacement
|
||||
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad');
|
||||
}
|
||||
await fs.ensureDir(path.dirname(targetFile));
|
||||
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
|
||||
|
||||
// Track the installed file
|
||||
this.installedFiles.add(targetFile);
|
||||
|
|
@ -1844,7 +1808,7 @@ class Installer {
|
|||
if (!(await fs.pathExists(customizePath))) {
|
||||
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
|
||||
if (await fs.pathExists(genericTemplatePath)) {
|
||||
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad');
|
||||
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
|
||||
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
||||
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
|
||||
}
|
||||
|
|
@ -1853,235 +1817,6 @@ class Installer {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build standalone agents in bmad/agents/ directory
|
||||
* @param {string} bmadDir - Path to bmad directory
|
||||
* @param {string} projectDir - Path to project directory
|
||||
*/
|
||||
async buildStandaloneAgents(bmadDir, projectDir) {
|
||||
const standaloneAgentsPath = path.join(bmadDir, 'agents');
|
||||
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
|
||||
|
||||
// Check if standalone agents directory exists
|
||||
if (!(await fs.pathExists(standaloneAgentsPath))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all subdirectories in agents/
|
||||
const agentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
|
||||
|
||||
for (const agentDir of agentDirs) {
|
||||
if (!agentDir.isDirectory()) continue;
|
||||
|
||||
const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
|
||||
|
||||
// Find any .agent.yaml file in the directory
|
||||
const files = await fs.readdir(agentDirPath);
|
||||
const yamlFile = files.find((f) => f.endsWith('.agent.yaml'));
|
||||
|
||||
if (!yamlFile) continue;
|
||||
|
||||
const agentName = path.basename(yamlFile, '.agent.yaml');
|
||||
const sourceYamlPath = path.join(agentDirPath, yamlFile);
|
||||
const targetMdPath = path.join(agentDirPath, `${agentName}.md`);
|
||||
const customizePath = path.join(cfgAgentsDir, `${agentName}.customize.yaml`);
|
||||
|
||||
// Check for customizations
|
||||
const customizeExists = await fs.pathExists(customizePath);
|
||||
let customizedFields = [];
|
||||
|
||||
if (customizeExists) {
|
||||
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
||||
const yaml = require('yaml');
|
||||
const customizeYaml = yaml.parse(customizeContent);
|
||||
|
||||
// Detect what fields are customized (similar to rebuildAgentFiles)
|
||||
if (customizeYaml) {
|
||||
if (customizeYaml.persona) {
|
||||
for (const [key, value] of Object.entries(customizeYaml.persona)) {
|
||||
if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
|
||||
customizedFields.push(`persona.${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (customizeYaml.agent?.metadata) {
|
||||
for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
|
||||
if (value !== '' && value !== null) {
|
||||
customizedFields.push(`metadata.${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
|
||||
customizedFields.push('critical_actions');
|
||||
}
|
||||
if (customizeYaml.menu && customizeYaml.menu.length > 0) {
|
||||
customizedFields.push('menu');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build YAML to XML .md
|
||||
let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
|
||||
includeMetadata: true,
|
||||
});
|
||||
|
||||
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
|
||||
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
|
||||
|
||||
// Process TTS injection points (pass targetPath for tracking)
|
||||
xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
|
||||
|
||||
// Write the built .md file with POSIX-compliant final newline
|
||||
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
|
||||
await fs.writeFile(targetMdPath, content, 'utf8');
|
||||
|
||||
// Display result
|
||||
if (customizedFields.length > 0) {
|
||||
console.log(chalk.dim(` Built standalone agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
|
||||
} else {
|
||||
console.log(chalk.dim(` Built standalone agent: ${agentName}.md`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild agent files from installer source (for compile command)
|
||||
* @param {string} modulePath - Path to module in bmad/ installation
|
||||
* @param {string} moduleName - Module name
|
||||
*/
|
||||
async rebuildAgentFiles(modulePath, moduleName) {
|
||||
// Get source agents directory from installer
|
||||
const sourceAgentsPath =
|
||||
moduleName === 'core' ? path.join(getModulePath('core'), 'agents') : path.join(getSourcePath(`modules/${moduleName}`), 'agents');
|
||||
|
||||
if (!(await fs.pathExists(sourceAgentsPath))) {
|
||||
return; // No source agents to rebuild
|
||||
}
|
||||
|
||||
// Determine project directory (parent of bmad/ directory)
|
||||
const bmadDir = path.dirname(modulePath);
|
||||
const projectDir = path.dirname(bmadDir);
|
||||
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
|
||||
const targetAgentsPath = path.join(modulePath, 'agents');
|
||||
|
||||
// Ensure target directory exists
|
||||
await fs.ensureDir(targetAgentsPath);
|
||||
|
||||
// Get all YAML agent files from source
|
||||
const sourceFiles = await fs.readdir(sourceAgentsPath);
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
if (file.endsWith('.agent.yaml')) {
|
||||
const agentName = file.replace('.agent.yaml', '');
|
||||
const sourceYamlPath = path.join(sourceAgentsPath, file);
|
||||
const targetMdPath = path.join(targetAgentsPath, `${agentName}.md`);
|
||||
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
|
||||
|
||||
// Check for customizations
|
||||
const customizeExists = await fs.pathExists(customizePath);
|
||||
let customizedFields = [];
|
||||
|
||||
if (customizeExists) {
|
||||
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
||||
const yaml = require('yaml');
|
||||
const customizeYaml = yaml.parse(customizeContent);
|
||||
|
||||
// Detect what fields are customized
|
||||
if (customizeYaml) {
|
||||
if (customizeYaml.persona) {
|
||||
for (const [key, value] of Object.entries(customizeYaml.persona)) {
|
||||
if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
|
||||
customizedFields.push(`persona.${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (customizeYaml.agent?.metadata) {
|
||||
for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
|
||||
if (value !== '' && value !== null) {
|
||||
customizedFields.push(`metadata.${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
|
||||
customizedFields.push('critical_actions');
|
||||
}
|
||||
if (customizeYaml.memories && customizeYaml.memories.length > 0) {
|
||||
customizedFields.push('memories');
|
||||
}
|
||||
if (customizeYaml.menu && customizeYaml.menu.length > 0) {
|
||||
customizedFields.push('menu');
|
||||
}
|
||||
if (customizeYaml.prompts && customizeYaml.prompts.length > 0) {
|
||||
customizedFields.push('prompts');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read the YAML content
|
||||
const yamlContent = await fs.readFile(sourceYamlPath, 'utf8');
|
||||
|
||||
// Read customize content if exists
|
||||
let customizeData = {};
|
||||
if (customizeExists) {
|
||||
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
||||
const yaml = require('yaml');
|
||||
customizeData = yaml.parse(customizeContent);
|
||||
}
|
||||
|
||||
// Build agent answers from customize data (filter empty values)
|
||||
const answers = {};
|
||||
if (customizeData.persona) {
|
||||
Object.assign(answers, filterCustomizationData(customizeData.persona));
|
||||
}
|
||||
if (customizeData.agent?.metadata) {
|
||||
const filteredMetadata = filterCustomizationData(customizeData.agent.metadata);
|
||||
if (Object.keys(filteredMetadata).length > 0) {
|
||||
Object.assign(answers, { metadata: filteredMetadata });
|
||||
}
|
||||
}
|
||||
if (customizeData.critical_actions && customizeData.critical_actions.length > 0) {
|
||||
answers.critical_actions = customizeData.critical_actions;
|
||||
}
|
||||
if (customizeData.memories && customizeData.memories.length > 0) {
|
||||
answers.memories = customizeData.memories;
|
||||
}
|
||||
|
||||
const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
|
||||
let coreConfig = {};
|
||||
if (await fs.pathExists(coreConfigPath)) {
|
||||
const yaml = require('yaml');
|
||||
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||
coreConfig = yaml.parse(coreConfigContent);
|
||||
}
|
||||
|
||||
// Compile using the same compiler as initial installation
|
||||
const { compileAgent } = require('../../../lib/agent/compiler');
|
||||
const result = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
|
||||
config: coreConfig,
|
||||
});
|
||||
|
||||
// Check if compilation succeeded
|
||||
if (!result || !result.xml) {
|
||||
throw new Error(`Failed to compile agent ${agentName}: No XML returned from compiler`);
|
||||
}
|
||||
|
||||
// Replace _bmad with actual folder name if needed
|
||||
const finalXml = result.xml.replaceAll('_bmad', path.basename(bmadDir));
|
||||
|
||||
// Write the rebuilt .md file with POSIX-compliant final newline
|
||||
const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n';
|
||||
await fs.writeFile(targetMdPath, content, 'utf8');
|
||||
|
||||
// Display result with customizations if any
|
||||
if (customizedFields.length > 0) {
|
||||
console.log(chalk.dim(` Rebuilt agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
|
||||
} else {
|
||||
console.log(chalk.dim(` Rebuilt agent: ${agentName}.md`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private: Update core
|
||||
*/
|
||||
|
|
@ -2677,190 +2412,6 @@ class Installer {
|
|||
return { customFiles, modifiedFiles };
|
||||
}
|
||||
|
||||
/**
|
||||
* Private: Create agent configuration files
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Object} userInfo - User information including name and language
|
||||
*/
|
||||
async createAgentConfigs(bmadDir, userInfo = null) {
|
||||
const agentConfigDir = path.join(bmadDir, '_config', 'agents');
|
||||
await fs.ensureDir(agentConfigDir);
|
||||
|
||||
// Get all agents from all modules
|
||||
const agents = [];
|
||||
const agentDetails = []; // For manifest generation
|
||||
|
||||
// Check modules for agents (including core)
|
||||
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name !== '_config') {
|
||||
const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents');
|
||||
if (await fs.pathExists(moduleAgentsPath)) {
|
||||
const agentFiles = await fs.readdir(moduleAgentsPath);
|
||||
for (const agentFile of agentFiles) {
|
||||
if (agentFile.endsWith('.md')) {
|
||||
const agentPath = path.join(moduleAgentsPath, agentFile);
|
||||
const agentContent = await fs.readFile(agentPath, 'utf8');
|
||||
|
||||
// Skip agents with localskip="true"
|
||||
const hasLocalSkip = agentContent.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
|
||||
if (hasLocalSkip) {
|
||||
continue; // Skip this agent - it should not have been installed
|
||||
}
|
||||
|
||||
const agentName = path.basename(agentFile, '.md');
|
||||
|
||||
// Extract any nodes with agentConfig="true"
|
||||
const agentConfigNodes = this.extractAgentConfigNodes(agentContent);
|
||||
|
||||
agents.push({
|
||||
name: agentName,
|
||||
module: entry.name,
|
||||
agentConfigNodes: agentConfigNodes,
|
||||
});
|
||||
|
||||
// Use shared AgentPartyGenerator to extract details
|
||||
let details = AgentPartyGenerator.extractAgentDetails(agentContent, entry.name, agentName);
|
||||
|
||||
// Apply config overrides if they exist
|
||||
if (details) {
|
||||
const configPath = path.join(agentConfigDir, `${entry.name}-${agentName}.md`);
|
||||
if (await fs.pathExists(configPath)) {
|
||||
const configContent = await fs.readFile(configPath, 'utf8');
|
||||
details = AgentPartyGenerator.applyConfigOverrides(details, configContent);
|
||||
}
|
||||
agentDetails.push(details);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create config file for each agent
|
||||
let createdCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
// Load agent config template
|
||||
const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md');
|
||||
const templateContent = await fs.readFile(templatePath, 'utf8');
|
||||
|
||||
for (const agent of agents) {
|
||||
const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`);
|
||||
|
||||
// Skip if config file already exists (preserve custom configurations)
|
||||
if (await fs.pathExists(configPath)) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build config content header
|
||||
let configContent = `# Agent Config: ${agent.name}\n\n`;
|
||||
|
||||
// Process template and add agent-specific config nodes
|
||||
let processedTemplate = templateContent;
|
||||
|
||||
// Replace {core:user_name} placeholder with actual user name if available
|
||||
if (userInfo && userInfo.userName) {
|
||||
processedTemplate = processedTemplate.replaceAll('{core:user_name}', userInfo.userName);
|
||||
}
|
||||
|
||||
// Replace {core:communication_language} placeholder with actual language if available
|
||||
if (userInfo && userInfo.responseLanguage) {
|
||||
processedTemplate = processedTemplate.replaceAll('{core:communication_language}', userInfo.responseLanguage);
|
||||
}
|
||||
|
||||
// If this agent has agentConfig nodes, add them after the existing comment
|
||||
if (agent.agentConfigNodes && agent.agentConfigNodes.length > 0) {
|
||||
// Find the agent-specific configuration nodes comment
|
||||
const commentPattern = /(\s*<!-- Agent-specific configuration nodes -->)/;
|
||||
const commentMatch = processedTemplate.match(commentPattern);
|
||||
|
||||
if (commentMatch) {
|
||||
// Add nodes right after the comment
|
||||
let agentSpecificNodes = '';
|
||||
for (const node of agent.agentConfigNodes) {
|
||||
agentSpecificNodes += `\n ${node}`;
|
||||
}
|
||||
|
||||
processedTemplate = processedTemplate.replace(commentPattern, `$1${agentSpecificNodes}`);
|
||||
}
|
||||
}
|
||||
|
||||
configContent += processedTemplate;
|
||||
|
||||
// Ensure POSIX-compliant final newline
|
||||
if (!configContent.endsWith('\n')) {
|
||||
configContent += '\n';
|
||||
}
|
||||
|
||||
await fs.writeFile(configPath, configContent, 'utf8');
|
||||
this.installedFiles.add(configPath); // Track agent config files
|
||||
createdCount++;
|
||||
}
|
||||
|
||||
// Generate agent manifest with overrides applied
|
||||
await this.generateAgentManifest(bmadDir, agentDetails);
|
||||
|
||||
return { total: agents.length, created: createdCount, skipped: skippedCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate agent manifest XML file
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Array} agentDetails - Array of agent details
|
||||
*/
|
||||
async generateAgentManifest(bmadDir, agentDetails) {
|
||||
const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
|
||||
await AgentPartyGenerator.writeAgentParty(manifestPath, agentDetails, { forWeb: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract nodes with agentConfig="true" from agent content
|
||||
* @param {string} content - Agent file content
|
||||
* @returns {Array} Array of XML nodes that should be added to agent config
|
||||
*/
|
||||
extractAgentConfigNodes(content) {
|
||||
const nodes = [];
|
||||
|
||||
try {
|
||||
// Find all XML nodes with agentConfig="true"
|
||||
// Match self-closing tags and tags with content
|
||||
const selfClosingPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*\/>/g;
|
||||
const withContentPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*>([\s\S]*?)<\/\1>/g;
|
||||
|
||||
// Extract self-closing tags
|
||||
let match;
|
||||
while ((match = selfClosingPattern.exec(content)) !== null) {
|
||||
// Extract just the tag without children (structure only)
|
||||
const tagMatch = match[0].match(/<([a-zA-Z][a-zA-Z0-9_-]*)([^>]*)\/>/);
|
||||
if (tagMatch) {
|
||||
const tagName = tagMatch[1];
|
||||
const attributes = tagMatch[2].replace(/\s*agentConfig="true"/, ''); // Remove agentConfig attribute
|
||||
nodes.push(`<${tagName}${attributes}></${tagName}>`);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract tags with content
|
||||
while ((match = withContentPattern.exec(content)) !== null) {
|
||||
const fullMatch = match[0];
|
||||
const tagName = match[1];
|
||||
|
||||
// Extract opening tag with attributes (removing agentConfig="true")
|
||||
const openingTagMatch = fullMatch.match(new RegExp(`<${tagName}([^>]*)>`));
|
||||
if (openingTagMatch) {
|
||||
const attributes = openingTagMatch[1].replace(/\s*agentConfig="true"/, '');
|
||||
// Add empty node structure (no children)
|
||||
nodes.push(`<${tagName}${attributes}></${tagName}>`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error extracting agentConfig nodes:', error);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle missing custom module sources interactively
|
||||
* @param {Map} customModuleSources - Map of custom module ID to info
|
||||
|
|
@ -2999,7 +2550,7 @@ class Installer {
|
|||
await this.manifest.addCustomModule(bmadDir, missing.info);
|
||||
|
||||
validCustomModules.push({
|
||||
id: moduleId,
|
||||
id: missing.id,
|
||||
name: missing.name,
|
||||
path: resolvedPath,
|
||||
info: missing.info,
|
||||
|
|
@ -3013,7 +2564,7 @@ class Installer {
|
|||
case 'remove': {
|
||||
// Extra confirmation for destructive remove
|
||||
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
|
||||
console.log(chalk.red(` Module location: ${path.join(bmadDir, moduleId)}`));
|
||||
console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
|
||||
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -65,11 +65,20 @@ class ManifestGenerator {
|
|||
// Filter out any undefined/null values from IDE list
|
||||
this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string');
|
||||
|
||||
// Get filtering options from options
|
||||
const selectedAgents = options.selectedAgents || null;
|
||||
const selectedWorkflows = options.selectedWorkflows || null;
|
||||
|
||||
// Store installation mode and options for manifest
|
||||
this.installMode = options.installMode || 'interactive';
|
||||
this.selectedAgentsList = selectedAgents;
|
||||
this.selectedWorkflowsList = selectedWorkflows;
|
||||
|
||||
// Collect workflow data
|
||||
await this.collectWorkflows(selectedModules);
|
||||
await this.collectWorkflows(selectedModules, selectedWorkflows);
|
||||
|
||||
// Collect agent data - use updatedModules which includes all installed modules
|
||||
await this.collectAgents(this.updatedModules);
|
||||
await this.collectAgents(this.updatedModules, selectedAgents);
|
||||
|
||||
// Collect task data
|
||||
await this.collectTasks(this.updatedModules);
|
||||
|
|
@ -100,8 +109,10 @@ class ManifestGenerator {
|
|||
/**
|
||||
* Collect all workflows from core and selected modules
|
||||
* Scans the INSTALLED bmad directory, not the source
|
||||
* @param {Array} selectedModules - Modules to scan for workflows
|
||||
* @param {Array|null} selectedWorkflows - Optional array of workflow names to filter by
|
||||
*/
|
||||
async collectWorkflows(selectedModules) {
|
||||
async collectWorkflows(selectedModules, selectedWorkflows = null) {
|
||||
this.workflows = [];
|
||||
|
||||
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
||||
|
|
@ -113,6 +124,50 @@ class ManifestGenerator {
|
|||
this.workflows.push(...moduleWorkflows);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filtering if selectedWorkflows is provided
|
||||
if (selectedWorkflows && Array.isArray(selectedWorkflows) && selectedWorkflows.length > 0) {
|
||||
this.workflows = this.filterWorkflows(this.workflows, selectedWorkflows);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter workflows by name matching (supports wildcards)
|
||||
* @param {Array} workflows - Array of workflow objects
|
||||
* @param {Array} selectedWorkflows - Array of workflow names to filter by (supports * wildcard)
|
||||
* @returns {Array} Filtered workflows
|
||||
*/
|
||||
filterWorkflows(workflows, selectedWorkflows) {
|
||||
if (!selectedWorkflows || selectedWorkflows.length === 0) {
|
||||
return workflows;
|
||||
}
|
||||
|
||||
// Check for special values
|
||||
if (selectedWorkflows.includes('all')) {
|
||||
return workflows;
|
||||
}
|
||||
if (selectedWorkflows.includes('none')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return workflows.filter((workflow) => {
|
||||
const workflowName = workflow.name;
|
||||
|
||||
return selectedWorkflows.some((pattern) => {
|
||||
// Exact match
|
||||
if (pattern === workflowName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wildcard matching: create-* matches create-prd, create-tech-spec, etc.
|
||||
if (pattern.includes('*')) {
|
||||
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
||||
return regex.test(workflowName);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -197,8 +252,10 @@ class ManifestGenerator {
|
|||
/**
|
||||
* Collect all agents from core and selected modules
|
||||
* Scans the INSTALLED bmad directory, not the source
|
||||
* @param {Array} selectedModules - Modules to scan for agents
|
||||
* @param {Array|null} selectedAgents - Optional array of agent names to filter by
|
||||
*/
|
||||
async collectAgents(selectedModules) {
|
||||
async collectAgents(selectedModules, selectedAgents = null) {
|
||||
this.agents = [];
|
||||
|
||||
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
||||
|
|
@ -224,6 +281,50 @@ class ManifestGenerator {
|
|||
this.agents.push(...standaloneAgents);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filtering if selectedAgents is provided
|
||||
if (selectedAgents && Array.isArray(selectedAgents) && selectedAgents.length > 0) {
|
||||
this.agents = this.filterAgents(this.agents, selectedAgents);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter agents by name matching (supports wildcards)
|
||||
* @param {Array} agents - Array of agent objects
|
||||
* @param {Array} selectedAgents - Array of agent names to filter by (supports * wildcard)
|
||||
* @returns {Array} Filtered agents
|
||||
*/
|
||||
filterAgents(agents, selectedAgents) {
|
||||
if (!selectedAgents || selectedAgents.length === 0) {
|
||||
return agents;
|
||||
}
|
||||
|
||||
// Check for special values
|
||||
if (selectedAgents.includes('all')) {
|
||||
return agents;
|
||||
}
|
||||
if (selectedAgents.includes('none')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return agents.filter((agent) => {
|
||||
const agentName = agent.name;
|
||||
|
||||
return selectedAgents.some((pattern) => {
|
||||
// Exact match
|
||||
if (pattern === agentName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wildcard matching: dev* matches dev, dev-story, etc.
|
||||
if (pattern.includes('*')) {
|
||||
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
||||
return regex.test(agentName);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -462,11 +563,20 @@ class ManifestGenerator {
|
|||
version: packageJson.version,
|
||||
installDate: new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
installMode: this.installMode || 'interactive',
|
||||
},
|
||||
modules: this.modules, // Include ALL modules (standard and custom)
|
||||
ides: this.selectedIdes,
|
||||
};
|
||||
|
||||
// Add selective installation info if filters were applied
|
||||
if (this.selectedAgentsList && this.selectedAgentsList.length > 0) {
|
||||
manifest.selectedAgents = this.selectedAgentsList;
|
||||
}
|
||||
if (this.selectedWorkflowsList && this.selectedWorkflowsList.length > 0) {
|
||||
manifest.selectedWorkflows = this.selectedWorkflowsList;
|
||||
}
|
||||
|
||||
// Clean the manifest to remove any non-serializable values
|
||||
const cleanManifest = structuredClone(manifest);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
const { getProfile } = require('../profiles/definitions');
|
||||
|
||||
/**
|
||||
* CLI Options Parser
|
||||
*
|
||||
* Parses and normalizes CLI options for non-interactive installation.
|
||||
* Handles profiles, comma-separated lists, special values, and validation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse comma-separated list into array
|
||||
* @param {string|undefined} value - Comma-separated string or undefined
|
||||
* @returns {string[]|null} Array of trimmed values or null if undefined
|
||||
*/
|
||||
function parseList(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if value is a special keyword
|
||||
* @param {string|string[]|null} value - Value to check
|
||||
* @returns {boolean} True if special keyword (all, none, minimal)
|
||||
*/
|
||||
function isSpecialValue(value) {
|
||||
if (Array.isArray(value) && value.length === 1) {
|
||||
value = value[0];
|
||||
}
|
||||
|
||||
return value === 'all' || value === 'none' || value === 'minimal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Separate additive (+) and subtractive (-) modifiers from list
|
||||
* @param {string[]} list - Array of items, some may have +/- prefix
|
||||
* @returns {Object} { base: [], add: [], remove: [] }
|
||||
*/
|
||||
function separateModifiers(list) {
|
||||
const result = {
|
||||
base: [],
|
||||
add: [],
|
||||
remove: [],
|
||||
};
|
||||
|
||||
if (!list || !Array.isArray(list)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const item of list) {
|
||||
if (item.startsWith('+')) {
|
||||
result.add.push(item.substring(1));
|
||||
} else if (item.startsWith('-')) {
|
||||
result.remove.push(item.substring(1));
|
||||
} else {
|
||||
result.base.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply modifiers to a base list (additive/subtractive)
|
||||
* @param {string[]} baseList - Base list of items
|
||||
* @param {string[]} add - Items to add
|
||||
* @param {string[]} remove - Items to remove
|
||||
* @returns {string[]} Modified list
|
||||
*/
|
||||
function applyModifiers(baseList, add = [], remove = []) {
|
||||
let result = [...baseList];
|
||||
|
||||
// Add items
|
||||
for (const item of add) {
|
||||
if (!result.includes(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove items
|
||||
for (const item of remove) {
|
||||
result = result.filter((i) => i !== item);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and normalize CLI options
|
||||
* @param {Object} cliOptions - Raw CLI options from commander
|
||||
* @returns {Object} Normalized options
|
||||
*/
|
||||
function parseOptions(cliOptions) {
|
||||
const normalized = {
|
||||
nonInteractive: cliOptions.nonInteractive || false,
|
||||
userName: cliOptions.userName,
|
||||
skillLevel: cliOptions.skillLevel,
|
||||
outputFolder: cliOptions.outputFolder,
|
||||
communicationLanguage: cliOptions.communicationLanguage,
|
||||
documentLanguage: cliOptions.documentLanguage,
|
||||
modules: parseList(cliOptions.modules),
|
||||
agents: parseList(cliOptions.agents),
|
||||
workflows: parseList(cliOptions.workflows),
|
||||
team: cliOptions.team,
|
||||
profile: cliOptions.profile,
|
||||
};
|
||||
|
||||
// Expand profile if provided
|
||||
if (normalized.profile) {
|
||||
const profile = getProfile(normalized.profile);
|
||||
if (!profile) {
|
||||
throw new Error(
|
||||
`Unknown profile: ${normalized.profile}. Valid profiles: minimal, full, solo-dev, team`,
|
||||
);
|
||||
}
|
||||
|
||||
// Profile provides defaults, but CLI options override
|
||||
normalized.profileModules = profile.modules;
|
||||
normalized.profileAgents = profile.agents;
|
||||
normalized.profileWorkflows = profile.workflows;
|
||||
|
||||
// If no explicit modules/agents/workflows, use profile values
|
||||
if (!normalized.modules) {
|
||||
normalized.modules = Array.isArray(profile.modules) ? profile.modules : profile.modules;
|
||||
}
|
||||
if (!normalized.agents) {
|
||||
normalized.agents = Array.isArray(profile.agents) ? profile.agents : profile.agents;
|
||||
}
|
||||
if (!normalized.workflows) {
|
||||
normalized.workflows = Array.isArray(profile.workflows)
|
||||
? profile.workflows
|
||||
: profile.workflows;
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate parsed options for conflicts and errors
|
||||
* @param {Object} options - Parsed options
|
||||
* @returns {Object} { valid: boolean, errors: string[] }
|
||||
*/
|
||||
function validateOptions(options) {
|
||||
const errors = [];
|
||||
|
||||
// Validate skill level
|
||||
if (options.skillLevel) {
|
||||
const validLevels = ['beginner', 'intermediate', 'advanced'];
|
||||
if (!validLevels.includes(options.skillLevel.toLowerCase())) {
|
||||
errors.push(
|
||||
`Invalid skill level: ${options.skillLevel}. Valid values: beginner, intermediate, advanced`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate profile
|
||||
if (options.profile) {
|
||||
const validProfiles = ['minimal', 'full', 'solo-dev', 'team'];
|
||||
if (!validProfiles.includes(options.profile.toLowerCase())) {
|
||||
errors.push(
|
||||
`Invalid profile: ${options.profile}. Valid values: minimal, full, solo-dev, team`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for empty selections
|
||||
if (options.agents && Array.isArray(options.agents) && options.agents.length === 0) {
|
||||
errors.push('Agents list cannot be empty');
|
||||
}
|
||||
|
||||
if (options.workflows && Array.isArray(options.workflows) && options.workflows.length === 0) {
|
||||
errors.push('Workflows list cannot be empty');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseList,
|
||||
isSpecialValue,
|
||||
separateModifiers,
|
||||
applyModifiers,
|
||||
parseOptions,
|
||||
validateOptions,
|
||||
};
|
||||
|
|
@ -731,7 +731,7 @@ class ModuleManager {
|
|||
async compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir, installer = null) {
|
||||
const sourceAgentsPath = path.join(sourcePath, 'agents');
|
||||
const targetAgentsPath = path.join(targetPath, 'agents');
|
||||
const cfgAgentsDir = path.join(bmadDir, '_bmad', '_config', 'agents');
|
||||
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
|
||||
|
||||
// Check if agents directory exists in source
|
||||
if (!(await fs.pathExists(sourceAgentsPath))) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* Installation Profile Definitions
|
||||
*
|
||||
* Profiles are pre-defined combinations of modules, agents, and workflows
|
||||
* for common use cases. Users can select a profile with --profile=<name>
|
||||
* and override specific selections with CLI flags.
|
||||
*/
|
||||
|
||||
const PROFILES = {
|
||||
minimal: {
|
||||
name: 'minimal',
|
||||
description: 'Minimal installation - core + dev agent + essential workflows',
|
||||
modules: ['core'],
|
||||
agents: ['dev'],
|
||||
workflows: ['create-tech-spec', 'quick-dev'],
|
||||
},
|
||||
|
||||
full: {
|
||||
name: 'full',
|
||||
description: 'Full installation - all modules, agents, and workflows',
|
||||
modules: 'all',
|
||||
agents: 'all',
|
||||
workflows: 'all',
|
||||
},
|
||||
|
||||
'solo-dev': {
|
||||
name: 'solo-dev',
|
||||
description: 'Single developer setup - dev tools and planning workflows',
|
||||
modules: ['core', 'bmm'],
|
||||
agents: ['dev', 'architect', 'analyst', 'tech-writer'],
|
||||
workflows: [
|
||||
'create-tech-spec',
|
||||
'quick-dev',
|
||||
'dev-story',
|
||||
'code-review',
|
||||
'create-prd',
|
||||
'create-architecture',
|
||||
],
|
||||
},
|
||||
|
||||
team: {
|
||||
name: 'team',
|
||||
description: 'Team collaboration setup - planning and execution workflows',
|
||||
modules: ['core', 'bmm'],
|
||||
agents: ['dev', 'architect', 'pm', 'sm', 'analyst', 'ux-designer'],
|
||||
workflows: [
|
||||
'create-product-brief',
|
||||
'create-prd',
|
||||
'create-architecture',
|
||||
'create-epics-and-stories',
|
||||
'sprint-planning',
|
||||
'create-story',
|
||||
'dev-story',
|
||||
'code-review',
|
||||
'workflow-init',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a profile by name
|
||||
* @param {string} name - Profile name (minimal, full, solo-dev, team)
|
||||
* @returns {Object|null} Profile definition or null if not found
|
||||
*/
|
||||
function getProfile(name) {
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profile = PROFILES[name.toLowerCase()];
|
||||
if (!profile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return a copy to prevent mutation
|
||||
return { ...profile };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available profile names
|
||||
* @returns {string[]} Array of profile names
|
||||
*/
|
||||
function getProfileNames() {
|
||||
return Object.keys(PROFILES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get profile descriptions for help text
|
||||
* @returns {Object} Map of profile name to description
|
||||
*/
|
||||
function getProfileDescriptions() {
|
||||
const descriptions = {};
|
||||
for (const [name, profile] of Object.entries(PROFILES)) {
|
||||
descriptions[name] = profile.description;
|
||||
}
|
||||
return descriptions;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getProfile,
|
||||
getProfileNames,
|
||||
getProfileDescriptions,
|
||||
};
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
const { glob } = require('glob');
|
||||
|
||||
/**
|
||||
* Team Loader
|
||||
*
|
||||
* Discovers and loads team bundles from module definitions.
|
||||
* Teams are predefined collections of agents and workflows for common use cases.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Discover all available teams across modules
|
||||
* @param {string} projectRoot - Project root directory
|
||||
* @returns {Promise<Object[]>} Array of team metadata { name, module, path, description }
|
||||
*/
|
||||
async function discoverTeams(projectRoot) {
|
||||
const teams = [];
|
||||
const pattern = path.join(projectRoot, 'src/modules/*/teams/team-*.yaml');
|
||||
|
||||
try {
|
||||
const files = await glob(pattern, { absolute: true });
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const teamData = yaml.parse(content);
|
||||
|
||||
// Extract team name from filename (team-fullstack.yaml -> fullstack)
|
||||
const filename = path.basename(filePath);
|
||||
const teamName = filename.replace(/^team-/, '').replace(/\.yaml$/, '');
|
||||
|
||||
// Extract module name from path
|
||||
const moduleName = path.basename(path.dirname(path.dirname(filePath)));
|
||||
|
||||
teams.push({
|
||||
name: teamName,
|
||||
module: moduleName,
|
||||
path: filePath,
|
||||
description: teamData.bundle?.description || 'No description',
|
||||
bundleName: teamData.bundle?.name || teamName,
|
||||
icon: teamData.bundle?.icon || '👥',
|
||||
});
|
||||
} catch (error) {
|
||||
// Skip files that can't be parsed
|
||||
console.warn(`Warning: Could not parse team file ${filePath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return teams;
|
||||
} catch (error) {
|
||||
// If glob fails, return empty array
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a specific team by name
|
||||
* @param {string} teamName - Team name (e.g., 'fullstack', 'gamedev')
|
||||
* @param {string} projectRoot - Project root directory
|
||||
* @returns {Promise<Object>} Team data with metadata
|
||||
*/
|
||||
async function loadTeam(teamName, projectRoot) {
|
||||
if (!teamName) {
|
||||
throw new Error('Team name is required');
|
||||
}
|
||||
|
||||
// Discover all teams
|
||||
const teams = await discoverTeams(projectRoot);
|
||||
|
||||
// Find matching team
|
||||
const team = teams.find((t) => t.name.toLowerCase() === teamName.toLowerCase());
|
||||
|
||||
if (!team) {
|
||||
// Provide helpful error with suggestions
|
||||
const availableTeams = teams.map((t) => t.name).join(', ');
|
||||
throw new Error(
|
||||
`Team '${teamName}' not found. Available teams: ${availableTeams || 'none'}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Load full team definition
|
||||
const content = fs.readFileSync(team.path, 'utf8');
|
||||
const teamData = yaml.parse(content);
|
||||
|
||||
return {
|
||||
name: team.name,
|
||||
module: team.module,
|
||||
description: team.description,
|
||||
bundleName: team.bundleName,
|
||||
icon: team.icon,
|
||||
agents: teamData.agents || [],
|
||||
workflows: teamData.workflows || [],
|
||||
party: teamData.party,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand team definition to full agents and workflows list
|
||||
* @param {string} teamName - Team name
|
||||
* @param {string} projectRoot - Project root directory
|
||||
* @returns {Promise<Object>} { agents: [], workflows: [], module: string }
|
||||
*/
|
||||
async function expandTeam(teamName, projectRoot) {
|
||||
const team = await loadTeam(teamName, projectRoot);
|
||||
|
||||
return {
|
||||
agents: team.agents || [],
|
||||
workflows: team.workflows || [],
|
||||
module: team.module,
|
||||
description: team.description,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply modifiers to team selections (additive/subtractive)
|
||||
* @param {Object} team - Team expansion result
|
||||
* @param {string[]} agentModifiers - Agent modifiers (+agent, -agent)
|
||||
* @param {string[]} workflowModifiers - Workflow modifiers (+workflow, -workflow)
|
||||
* @returns {Object} Modified team with updated agents/workflows
|
||||
*/
|
||||
function applyTeamModifiers(team, agentModifiers = [], workflowModifiers = []) {
|
||||
const result = {
|
||||
...team,
|
||||
agents: [...team.agents],
|
||||
workflows: [...team.workflows],
|
||||
};
|
||||
|
||||
// Parse and apply agent modifiers
|
||||
for (const modifier of agentModifiers) {
|
||||
if (modifier.startsWith('+')) {
|
||||
const agent = modifier.substring(1);
|
||||
if (!result.agents.includes(agent)) {
|
||||
result.agents.push(agent);
|
||||
}
|
||||
} else if (modifier.startsWith('-')) {
|
||||
const agent = modifier.substring(1);
|
||||
result.agents = result.agents.filter((a) => a !== agent);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and apply workflow modifiers
|
||||
for (const modifier of workflowModifiers) {
|
||||
if (modifier.startsWith('+')) {
|
||||
const workflow = modifier.substring(1);
|
||||
if (!result.workflows.includes(workflow)) {
|
||||
result.workflows.push(workflow);
|
||||
}
|
||||
} else if (modifier.startsWith('-')) {
|
||||
const workflow = modifier.substring(1);
|
||||
result.workflows = result.workflows.filter((w) => w !== workflow);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get team descriptions for help text
|
||||
* @param {string} projectRoot - Project root directory
|
||||
* @returns {Promise<Object>} Map of team name to description
|
||||
*/
|
||||
async function getTeamDescriptions(projectRoot) {
|
||||
const teams = await discoverTeams(projectRoot);
|
||||
const descriptions = {};
|
||||
|
||||
for (const team of teams) {
|
||||
descriptions[team.name] = team.description;
|
||||
}
|
||||
|
||||
return descriptions;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
discoverTeams,
|
||||
loadTeam,
|
||||
expandTeam,
|
||||
applyTeamModifiers,
|
||||
getTeamDescriptions,
|
||||
};
|
||||
|
|
@ -12,9 +12,15 @@ const { CustomHandler } = require('../installers/lib/custom/handler');
|
|||
class UI {
|
||||
/**
|
||||
* Prompt for installation configuration
|
||||
* @param {Object} cliOptions - CLI options for non-interactive mode
|
||||
* @returns {Object} Installation configuration
|
||||
*/
|
||||
async promptInstall() {
|
||||
async promptInstall(cliOptions = {}) {
|
||||
// Handle non-interactive mode
|
||||
if (cliOptions.nonInteractive) {
|
||||
return await this.buildNonInteractiveConfig(cliOptions);
|
||||
}
|
||||
|
||||
CLIUtils.displayLogo();
|
||||
|
||||
// Display changelog link
|
||||
|
|
@ -1457,6 +1463,139 @@ class UI {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build non-interactive installation configuration
|
||||
* @param {Object} cliOptions - CLI options
|
||||
* @returns {Object} Installation configuration
|
||||
*/
|
||||
async buildNonInteractiveConfig(cliOptions) {
|
||||
const { parseOptions } = require('../installers/lib/core/options-parser');
|
||||
const { expandTeam, applyTeamModifiers } = require('../installers/lib/teams/team-loader');
|
||||
const { getProjectRoot } = require('./project-root');
|
||||
const { getEnvironmentDefaults } = require('../installers/lib/core/env-resolver');
|
||||
|
||||
console.log(chalk.cyan('🤖 Running non-interactive installation...\n'));
|
||||
|
||||
// Parse and normalize options
|
||||
const options = parseOptions(cliOptions);
|
||||
const envDefaults = getEnvironmentDefaults();
|
||||
|
||||
// Determine directory
|
||||
const directory = process.cwd();
|
||||
|
||||
// Check for existing installation
|
||||
const { Installer } = require('../installers/lib/core/installer');
|
||||
const installer = new Installer();
|
||||
const { bmadDir, hasExistingInstall } = await installer.findBmadDir(directory);
|
||||
const actionType = hasExistingInstall ? 'update' : 'install';
|
||||
|
||||
console.log(chalk.dim(` Directory: ${directory}`));
|
||||
console.log(chalk.dim(` Action: ${actionType === 'install' ? 'New installation' : 'Update existing installation'}\n`));
|
||||
|
||||
// Determine modules to install
|
||||
let selectedModules = [];
|
||||
|
||||
if (options.team) {
|
||||
// Team-based installation
|
||||
console.log(chalk.cyan(`📦 Loading team: ${options.team}...`));
|
||||
try {
|
||||
const projectRoot = getProjectRoot();
|
||||
let teamExpansion = await expandTeam(options.team, projectRoot);
|
||||
|
||||
// Apply modifiers if present
|
||||
if (options.agents || options.workflows) {
|
||||
const { separateModifiers } = require('../installers/lib/core/options-parser');
|
||||
const agentMods = options.agents ? separateModifiers(options.agents) : { base: [], add: [], remove: [] };
|
||||
const workflowMods = options.workflows ? separateModifiers(options.workflows) : { base: [], add: [], remove: [] };
|
||||
|
||||
// If base is provided, replace team selections completely
|
||||
if (agentMods.base.length > 0) {
|
||||
teamExpansion.agents = agentMods.base;
|
||||
}
|
||||
if (workflowMods.base.length > 0) {
|
||||
teamExpansion.workflows = workflowMods.base;
|
||||
}
|
||||
|
||||
// Apply modifiers
|
||||
teamExpansion = applyTeamModifiers(
|
||||
teamExpansion,
|
||||
[...agentMods.add.map((a) => `+${a}`), ...agentMods.remove.map((a) => `-${a}`)],
|
||||
[...workflowMods.add.map((w) => `+${w}`), ...workflowMods.remove.map((w) => `-${w}`)],
|
||||
);
|
||||
}
|
||||
|
||||
options.agents = teamExpansion.agents;
|
||||
options.workflows = teamExpansion.workflows;
|
||||
|
||||
// Determine module from team
|
||||
if (teamExpansion.module && !selectedModules.includes(teamExpansion.module)) {
|
||||
selectedModules.push(teamExpansion.module);
|
||||
}
|
||||
|
||||
console.log(chalk.green(` ✓ Team loaded: ${options.team}`));
|
||||
console.log(chalk.dim(` Agents: ${teamExpansion.agents.join(', ')}`));
|
||||
if (teamExpansion.workflows && teamExpansion.workflows.length > 0) {
|
||||
console.log(chalk.dim(` Workflows: ${teamExpansion.workflows.join(', ')}`));
|
||||
}
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(chalk.red(` ✗ Failed to load team: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (options.modules) {
|
||||
// Module-based installation
|
||||
if (options.modules === 'all') {
|
||||
selectedModules = ['bmm', 'bmbb', 'cis', 'bmgd'];
|
||||
} else if (Array.isArray(options.modules)) {
|
||||
selectedModules = options.modules.filter((m) => m !== 'core');
|
||||
}
|
||||
} else if (options.profile) {
|
||||
// Profile-based installation
|
||||
const { getProfile } = require('../installers/lib/profiles/definitions');
|
||||
const profile = getProfile(options.profile);
|
||||
if (profile.modules === 'all') {
|
||||
selectedModules = ['bmm', 'bmbb', 'cis', 'bmgd'];
|
||||
} else if (Array.isArray(profile.modules)) {
|
||||
selectedModules = profile.modules.filter((m) => m !== 'core');
|
||||
}
|
||||
} else {
|
||||
// Default: install bmm
|
||||
selectedModules = ['bmm'];
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`📦 Modules: ${selectedModules.length > 0 ? selectedModules.join(', ') : 'core only'}\n`));
|
||||
|
||||
// Build core configuration
|
||||
const coreConfig = {
|
||||
user_name: options.userName || envDefaults.userName,
|
||||
user_skill_level: options.skillLevel || 'intermediate',
|
||||
communication_language: options.communicationLanguage || envDefaults.communicationLanguage,
|
||||
document_output_language: options.documentLanguage || envDefaults.documentLanguage,
|
||||
output_folder: options.outputFolder || '_bmad-output',
|
||||
};
|
||||
|
||||
console.log(chalk.cyan('⚙️ Configuration:'));
|
||||
console.log(chalk.dim(` User: ${coreConfig.user_name}`));
|
||||
console.log(chalk.dim(` Skill Level: ${coreConfig.user_skill_level}`));
|
||||
console.log(chalk.dim(` Language: ${coreConfig.communication_language}\n`));
|
||||
|
||||
// Return installation configuration
|
||||
return {
|
||||
actionType,
|
||||
directory,
|
||||
installCore: true,
|
||||
modules: selectedModules,
|
||||
ides: ['claude-code'], // Default to Claude Code for non-interactive
|
||||
skipIde: false,
|
||||
coreConfig,
|
||||
customContent: { hasCustomContent: false },
|
||||
enableAgentVibes: false,
|
||||
agentVibesInstalled: false,
|
||||
// Pass through CLI options for downstream use
|
||||
cliOptions: options,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UI };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
#!/bin/bash
|
||||
# Test script for non-interactive BMAD installation
|
||||
# Tests various CLI options and validates installation
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
TEST_DIR="/tmp/bmad-test-$(date +%s)"
|
||||
|
||||
echo "🧪 BMAD Non-Interactive Installation Test Suite"
|
||||
echo "================================================"
|
||||
echo "Test directory: $TEST_DIR"
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Track test results
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Helper function to run a test
|
||||
run_test() {
|
||||
local test_name="$1"
|
||||
local test_dir="$TEST_DIR/$test_name"
|
||||
shift
|
||||
|
||||
echo -e "${YELLOW}▶ Running: $test_name${NC}"
|
||||
mkdir -p "$test_dir"
|
||||
cd "$test_dir"
|
||||
|
||||
if "$@"; then
|
||||
echo -e "${GREEN}✓ PASSED: $test_name${NC}"
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗ FAILED: $test_name${NC}"
|
||||
TESTS_FAILED=$((TESTS_FAILED + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper to verify installation
|
||||
verify_installation() {
|
||||
local dir="$1"
|
||||
local expected_agents="$2" # comma-separated list
|
||||
local expected_workflows="$3" # comma-separated list
|
||||
|
||||
# Check _bmad directory exists
|
||||
if [ ! -d "$dir/_bmad" ]; then
|
||||
echo "❌ _bmad directory not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check manifest exists
|
||||
if [ ! -f "$dir/_bmad/_config/manifest.yaml" ]; then
|
||||
echo "❌ manifest.yaml not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check agents CSV if expected agents provided
|
||||
if [ -n "$expected_agents" ]; then
|
||||
if [ ! -f "$dir/_bmad/_config/agents.csv" ]; then
|
||||
echo "❌ agents.csv not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
IFS=',' read -ra AGENTS <<< "$expected_agents"
|
||||
for agent in "${AGENTS[@]}"; do
|
||||
if ! grep -q "$agent" "$dir/_bmad/_config/agents.csv"; then
|
||||
echo "❌ Agent '$agent' not found in agents.csv"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Check workflows CSV if expected workflows provided
|
||||
if [ -n "$expected_workflows" ]; then
|
||||
if [ ! -f "$dir/_bmad/_config/workflows.csv" ]; then
|
||||
echo "❌ workflows.csv not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
IFS=',' read -ra WORKFLOWS <<< "$expected_workflows"
|
||||
for workflow in "${WORKFLOWS[@]}"; do
|
||||
if ! grep -q "$workflow" "$dir/_bmad/_config/workflows.csv"; then
|
||||
echo "❌ Workflow '$workflow' not found in workflows.csv"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "✓ Installation verified"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test 1: Minimal non-interactive installation
|
||||
run_test "test-01-minimal-install" bash -c "
|
||||
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y
|
||||
verify_installation . '' ''
|
||||
"
|
||||
|
||||
# Test 2: Non-interactive with custom user name
|
||||
run_test "test-02-custom-user" bash -c "
|
||||
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --user-name=TestUser
|
||||
verify_installation . '' ''
|
||||
grep -q 'user_name: TestUser' _bmad/core/config.yaml
|
||||
"
|
||||
|
||||
# Test 3: Selective agent installation
|
||||
run_test "test-03-selective-agents" bash -c "
|
||||
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --agents=dev,architect
|
||||
verify_installation . 'dev,architect' ''
|
||||
"
|
||||
|
||||
# Test 4: Selective workflow installation
|
||||
run_test "test-04-selective-workflows" bash -c "
|
||||
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --workflows=create-prd,create-tech-spec
|
||||
verify_installation . '' 'create-prd,create-tech-spec'
|
||||
"
|
||||
|
||||
# Test 5: Team-based installation (fullstack)
|
||||
run_test "test-05-team-fullstack" bash -c "
|
||||
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --team=fullstack
|
||||
verify_installation . 'analyst,architect,pm,sm,ux-designer' ''
|
||||
"
|
||||
|
||||
# Test 6: Profile-based installation (minimal)
|
||||
run_test "test-06-profile-minimal" bash -c "
|
||||
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --profile=minimal
|
||||
verify_installation . 'dev' 'create-tech-spec,quick-dev'
|
||||
"
|
||||
|
||||
# Test 7: Multiple CLI options
|
||||
run_test "test-07-multiple-options" bash -c "
|
||||
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y \
|
||||
--user-name=FullTest \
|
||||
--skill-level=advanced \
|
||||
--output-folder=.output \
|
||||
--agents=dev,architect
|
||||
verify_installation . 'dev,architect' ''
|
||||
grep -q 'user_name: FullTest' _bmad/core/config.yaml
|
||||
grep -q 'user_skill_level: advanced' _bmad/core/config.yaml
|
||||
"
|
||||
|
||||
# Test 8: Manifest tracking
|
||||
run_test "test-08-manifest-tracking" bash -c "
|
||||
node $PROJECT_ROOT/tools/bmad-npx-wrapper.js install -y --agents=dev
|
||||
verify_installation . 'dev' ''
|
||||
grep -q 'installMode: non-interactive' _bmad/_config/manifest.yaml
|
||||
grep -q 'selectedAgents:' _bmad/_config/manifest.yaml
|
||||
"
|
||||
|
||||
# Cleanup
|
||||
echo ""
|
||||
echo "🧹 Cleaning up test directory: $TEST_DIR"
|
||||
rm -rf "$TEST_DIR"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "Test Summary"
|
||||
echo "================================================"
|
||||
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
|
||||
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
|
||||
echo ""
|
||||
|
||||
if [ $TESTS_FAILED -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ All tests passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Some tests failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
Loading…
Reference in New Issue