Merge 52bd250a8c into 23f650ff4d
This commit is contained in:
commit
7d705aa08b
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
|
### 1. Install BMad Method
|
||||||
|
|
||||||
|
#### Interactive Installation (Default)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install v6 Alpha (recommended)
|
# Install v6 Alpha (recommended)
|
||||||
npx bmad-method@alpha install
|
npx bmad-method@alpha install
|
||||||
|
|
@ -87,6 +89,65 @@ npx bmad-method@alpha install
|
||||||
npx bmad-method install
|
npx bmad-method install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Non-Interactive Installation (CI/CD, Automation)
|
||||||
|
|
||||||
|
For automated deployments and CI/CD pipelines:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Minimal installation with defaults
|
||||||
|
npx bmad-method@alpha install -y
|
||||||
|
|
||||||
|
# Custom configuration
|
||||||
|
npx bmad-method@alpha install -y \
|
||||||
|
--user-name=Alice \
|
||||||
|
--skill-level=advanced \
|
||||||
|
--output-folder=.bmad-output
|
||||||
|
|
||||||
|
# Team-based installation (fullstack team)
|
||||||
|
npx bmad-method@alpha install -y --team=fullstack
|
||||||
|
|
||||||
|
# Team with modifications
|
||||||
|
npx bmad-method@alpha install -y --team=fullstack --agents=+dev
|
||||||
|
|
||||||
|
# Selective agents and workflows
|
||||||
|
npx bmad-method@alpha install -y \
|
||||||
|
--agents=dev,architect,pm \
|
||||||
|
--workflows=create-prd,create-tech-spec,dev-story
|
||||||
|
|
||||||
|
# Profile-based installation
|
||||||
|
npx bmad-method@alpha install -y --profile=minimal
|
||||||
|
npx bmad-method@alpha install -y --profile=solo-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available Options:**
|
||||||
|
|
||||||
|
- `-y, --non-interactive` - Skip all prompts, use defaults
|
||||||
|
- `--user-name <name>` - User name for configuration
|
||||||
|
- `--skill-level <level>` - beginner, intermediate, advanced
|
||||||
|
- `--output-folder <path>` - Output folder for BMAD artifacts
|
||||||
|
- `--modules <list>` - Comma-separated module list
|
||||||
|
- `--agents <list>` - Comma-separated agent list or 'all', 'none'
|
||||||
|
- `--workflows <list>` - Comma-separated workflow list or 'all', 'none'
|
||||||
|
- `--team <name>` - Install predefined team (fullstack, gamedev)
|
||||||
|
- `--profile <name>` - Installation profile (minimal, full, solo-dev, team)
|
||||||
|
|
||||||
|
**Modifiers:**
|
||||||
|
|
||||||
|
- `--agents=+dev` - Add agent to team/profile selection
|
||||||
|
- `--agents=-dev` - Remove agent from team/profile selection
|
||||||
|
|
||||||
|
**Available Teams:**
|
||||||
|
|
||||||
|
- `fullstack` - analyst, architect, pm, sm, ux-designer
|
||||||
|
- `gamedev` - game-designer, game-dev, game-architect, game-scrum-master
|
||||||
|
|
||||||
|
**Available Profiles:**
|
||||||
|
|
||||||
|
- `minimal` - Core + dev agent + essential workflows
|
||||||
|
- `full` - Everything (all modules, agents, workflows)
|
||||||
|
- `solo-dev` - Single developer setup
|
||||||
|
- `team` - Team collaboration setup
|
||||||
|
|
||||||
### 2. Initialize Your Project
|
### 2. Initialize Your Project
|
||||||
|
|
||||||
Load any agent in your IDE and run:
|
Load any agent in your IDE and run:
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
module.exports = {
|
||||||
command: 'install',
|
command: 'install',
|
||||||
description: 'Install BMAD Core agents and tools',
|
description: `Install BMAD Core agents and tools
|
||||||
options: [],
|
|
||||||
|
Examples:
|
||||||
|
bmad install # Interactive installation
|
||||||
|
bmad install -y # Non-interactive with defaults
|
||||||
|
bmad install -y --user-name=Alice --skill-level=advanced
|
||||||
|
bmad install -y --team=fullstack # Install fullstack team
|
||||||
|
bmad install -y --team=fullstack --agents=+dev # Add dev to fullstack team
|
||||||
|
bmad install -y --agents=dev,architect,pm # Selective agents
|
||||||
|
bmad install -y --profile=minimal # Minimal profile
|
||||||
|
bmad install -y --workflows=create-prd,dev-story # Selective workflows
|
||||||
|
|
||||||
|
Special Values:
|
||||||
|
--agents=all, --agents=none, --agents=minimal
|
||||||
|
--workflows=all, --workflows=none, --workflows=minimal
|
||||||
|
|
||||||
|
Modifiers:
|
||||||
|
--agents=+dev Add agent to team/profile selection
|
||||||
|
--agents=-dev Remove agent from team/profile selection
|
||||||
|
|
||||||
|
Available Teams: fullstack, gamedev
|
||||||
|
Available Profiles: minimal, full, solo-dev, team`,
|
||||||
|
options: [
|
||||||
|
['-y, --non-interactive', 'Run without prompts, use defaults'],
|
||||||
|
['--user-name <name>', 'User name for configuration'],
|
||||||
|
['--skill-level <level>', 'User skill level (beginner, intermediate, advanced)'],
|
||||||
|
['--output-folder <path>', 'Output folder path for BMAD artifacts'],
|
||||||
|
['--modules <list>', 'Comma-separated list of modules to install (e.g., core,bmm)'],
|
||||||
|
['--agents <list>', 'Comma-separated list of agents to install (e.g., dev,architect,pm)'],
|
||||||
|
['--workflows <list>', 'Comma-separated list of workflows to install'],
|
||||||
|
['--team <name>', 'Install predefined team bundle (e.g., fullstack, gamedev)'],
|
||||||
|
['--profile <name>', 'Installation profile (minimal, full, solo-dev)'],
|
||||||
|
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
||||||
|
['--document-language <lang>', 'Language for generated documents (default: English)'],
|
||||||
|
],
|
||||||
action: async (options) => {
|
action: async (options) => {
|
||||||
try {
|
try {
|
||||||
const config = await ui.promptInstall();
|
const config = await ui.promptInstall(options);
|
||||||
|
|
||||||
// Handle cancel
|
// Handle cancel
|
||||||
if (config.actionType === 'cancel') {
|
if (config.actionType === 'cancel') {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const chalk = require('chalk');
|
||||||
const inquirer = require('inquirer');
|
const inquirer = require('inquirer');
|
||||||
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
|
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
|
||||||
const { CLIUtils } = require('../../../lib/cli-utils');
|
const { CLIUtils } = require('../../../lib/cli-utils');
|
||||||
|
const { getEnvironmentDefaults, resolveValue } = require('./env-resolver');
|
||||||
|
|
||||||
class ConfigCollector {
|
class ConfigCollector {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -792,6 +793,180 @@ class ConfigCollector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect configuration for a module in non-interactive mode
|
||||||
|
* @param {string} moduleName - Module name
|
||||||
|
* @param {string} projectDir - Target project directory
|
||||||
|
* @param {Object} cliOptions - CLI options passed by user
|
||||||
|
* @returns {Object} Collected config for the module
|
||||||
|
*/
|
||||||
|
async collectModuleConfigNonInteractive(moduleName, projectDir, cliOptions = {}) {
|
||||||
|
this.currentProjectDir = projectDir;
|
||||||
|
|
||||||
|
// Load existing config if not already loaded
|
||||||
|
if (!this.existingConfig) {
|
||||||
|
await this.loadExistingConfig(projectDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize allAnswers if not already initialized
|
||||||
|
if (!this.allAnswers) {
|
||||||
|
this.allAnswers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get environment defaults
|
||||||
|
const envDefaults = getEnvironmentDefaults();
|
||||||
|
|
||||||
|
// Try to load module config schema
|
||||||
|
let installerConfigPath = null;
|
||||||
|
let moduleConfigPath = null;
|
||||||
|
|
||||||
|
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
|
||||||
|
const customPath = this.customModulePaths.get(moduleName);
|
||||||
|
installerConfigPath = path.join(customPath, '_module-installer', 'module.yaml');
|
||||||
|
moduleConfigPath = path.join(customPath, 'module.yaml');
|
||||||
|
} else {
|
||||||
|
installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
|
||||||
|
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, try to find via module manager
|
||||||
|
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
|
||||||
|
const { ModuleManager } = require('../modules/manager');
|
||||||
|
const moduleManager = new ModuleManager();
|
||||||
|
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||||
|
|
||||||
|
if (moduleSourcePath) {
|
||||||
|
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
|
||||||
|
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let configPath = null;
|
||||||
|
if (await fs.pathExists(moduleConfigPath)) {
|
||||||
|
configPath = moduleConfigPath;
|
||||||
|
} else if (await fs.pathExists(installerConfigPath)) {
|
||||||
|
configPath = installerConfigPath;
|
||||||
|
} else {
|
||||||
|
// No config for this module - use existing if available
|
||||||
|
if (this.existingConfig && this.existingConfig[moduleName]) {
|
||||||
|
this.collectedConfig[moduleName] = { ...this.existingConfig[moduleName] };
|
||||||
|
} else {
|
||||||
|
this.collectedConfig[moduleName] = {};
|
||||||
|
}
|
||||||
|
return this.collectedConfig[moduleName];
|
||||||
|
}
|
||||||
|
|
||||||
|
const configContent = await fs.readFile(configPath, 'utf8');
|
||||||
|
const moduleConfig = yaml.parse(configContent);
|
||||||
|
|
||||||
|
if (!moduleConfig) {
|
||||||
|
this.collectedConfig[moduleName] = {};
|
||||||
|
return this.collectedConfig[moduleName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize module config
|
||||||
|
if (!this.collectedConfig[moduleName]) {
|
||||||
|
this.collectedConfig[moduleName] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each config item
|
||||||
|
const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
|
||||||
|
|
||||||
|
for (const key of configKeys) {
|
||||||
|
const item = moduleConfig[key];
|
||||||
|
|
||||||
|
// Skip if not a config object
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = null;
|
||||||
|
|
||||||
|
// Resolution order: CLI → ENV → existing → default → hardcoded
|
||||||
|
if (moduleName === 'core') {
|
||||||
|
// Core module has special mappings
|
||||||
|
if (key === 'user_name') {
|
||||||
|
value = resolveValue(cliOptions.userName, null, envDefaults.userName);
|
||||||
|
} else if (key === 'user_skill_level') {
|
||||||
|
value = resolveValue(cliOptions.skillLevel, null, item.default || 'intermediate');
|
||||||
|
} else if (key === 'communication_language') {
|
||||||
|
value = resolveValue(
|
||||||
|
cliOptions.communicationLanguage,
|
||||||
|
null,
|
||||||
|
envDefaults.communicationLanguage,
|
||||||
|
);
|
||||||
|
} else if (key === 'document_output_language') {
|
||||||
|
value = resolveValue(cliOptions.documentLanguage, null, envDefaults.documentLanguage);
|
||||||
|
} else if (key === 'output_folder') {
|
||||||
|
value = resolveValue(cliOptions.outputFolder, null, item.default);
|
||||||
|
} else if (item.default !== undefined) {
|
||||||
|
value = item.default;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For other modules, use defaults
|
||||||
|
if (item.default !== undefined) {
|
||||||
|
value = item.default;
|
||||||
|
} else if (this.existingConfig && this.existingConfig[moduleName]) {
|
||||||
|
value = this.existingConfig[moduleName][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process result template if present
|
||||||
|
let result;
|
||||||
|
if (item.result && typeof item.result === 'string') {
|
||||||
|
result = item.result;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
result = result.replace('{value}', value);
|
||||||
|
} else if (value !== undefined && value !== null) {
|
||||||
|
result = result.replace('{value}', String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace references to other config values
|
||||||
|
result = result.replaceAll(/{([^}]+)}/g, (match, configKey) => {
|
||||||
|
if (configKey === 'project-root' || configKey === 'value') {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look in collected config
|
||||||
|
let configValue = this.collectedConfig[moduleName]?.[configKey];
|
||||||
|
if (!configValue) {
|
||||||
|
for (const mod of Object.keys(this.collectedConfig)) {
|
||||||
|
if (mod !== '_meta' && this.collectedConfig[mod]?.[configKey]) {
|
||||||
|
configValue = this.collectedConfig[mod][configKey];
|
||||||
|
if (typeof configValue === 'string' && configValue.includes('{project-root}/')) {
|
||||||
|
configValue = configValue.replace('{project-root}/', '');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configValue || match;
|
||||||
|
});
|
||||||
|
} else if (item.result) {
|
||||||
|
result = value;
|
||||||
|
} else {
|
||||||
|
result = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the result
|
||||||
|
this.collectedConfig[moduleName][key] = result;
|
||||||
|
this.allAnswers[`${moduleName}_${key}`] = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy existing values for fields not in schema
|
||||||
|
if (this.existingConfig && this.existingConfig[moduleName]) {
|
||||||
|
for (const [key, value] of Object.entries(this.existingConfig[moduleName])) {
|
||||||
|
if (this.collectedConfig[moduleName][key] === undefined) {
|
||||||
|
this.collectedConfig[moduleName][key] = value;
|
||||||
|
this.allAnswers[`${moduleName}_${key}`] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.collectedConfig[moduleName];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace placeholders in a string with collected config values
|
* Replace placeholders in a string with collected config values
|
||||||
* @param {string} str - String with placeholders
|
* @param {string} str - String with placeholders
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -1041,6 +1041,9 @@ class Installer {
|
||||||
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
|
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
|
||||||
ides: config.ides || [],
|
ides: config.ides || [],
|
||||||
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
|
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
|
||||||
|
selectedAgents: config.cliOptions?.agents || null,
|
||||||
|
selectedWorkflows: config.cliOptions?.workflows || null,
|
||||||
|
installMode: config.cliOptions ? 'non-interactive' : 'interactive',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Custom modules are now included in the main modules list - no separate tracking needed
|
// Custom modules are now included in the main modules list - no separate tracking needed
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,20 @@ class ManifestGenerator {
|
||||||
// Filter out any undefined/null values from IDE list
|
// Filter out any undefined/null values from IDE list
|
||||||
this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string');
|
this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string');
|
||||||
|
|
||||||
|
// Get filtering options from options
|
||||||
|
const selectedAgents = options.selectedAgents || null;
|
||||||
|
const selectedWorkflows = options.selectedWorkflows || null;
|
||||||
|
|
||||||
|
// Store installation mode and options for manifest
|
||||||
|
this.installMode = options.installMode || 'interactive';
|
||||||
|
this.selectedAgentsList = selectedAgents;
|
||||||
|
this.selectedWorkflowsList = selectedWorkflows;
|
||||||
|
|
||||||
// Collect workflow data
|
// Collect workflow data
|
||||||
await this.collectWorkflows(selectedModules);
|
await this.collectWorkflows(selectedModules, selectedWorkflows);
|
||||||
|
|
||||||
// Collect agent data - use updatedModules which includes all installed modules
|
// Collect agent data - use updatedModules which includes all installed modules
|
||||||
await this.collectAgents(this.updatedModules);
|
await this.collectAgents(this.updatedModules, selectedAgents);
|
||||||
|
|
||||||
// Collect task data
|
// Collect task data
|
||||||
await this.collectTasks(this.updatedModules);
|
await this.collectTasks(this.updatedModules);
|
||||||
|
|
@ -100,8 +109,10 @@ class ManifestGenerator {
|
||||||
/**
|
/**
|
||||||
* Collect all workflows from core and selected modules
|
* Collect all workflows from core and selected modules
|
||||||
* Scans the INSTALLED bmad directory, not the source
|
* Scans the INSTALLED bmad directory, not the source
|
||||||
|
* @param {Array} selectedModules - Modules to scan for workflows
|
||||||
|
* @param {Array|null} selectedWorkflows - Optional array of workflow names to filter by
|
||||||
*/
|
*/
|
||||||
async collectWorkflows(selectedModules) {
|
async collectWorkflows(selectedModules, selectedWorkflows = null) {
|
||||||
this.workflows = [];
|
this.workflows = [];
|
||||||
|
|
||||||
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
||||||
|
|
@ -113,6 +124,50 @@ class ManifestGenerator {
|
||||||
this.workflows.push(...moduleWorkflows);
|
this.workflows.push(...moduleWorkflows);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply filtering if selectedWorkflows is provided
|
||||||
|
if (selectedWorkflows && Array.isArray(selectedWorkflows) && selectedWorkflows.length > 0) {
|
||||||
|
this.workflows = this.filterWorkflows(this.workflows, selectedWorkflows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter workflows by name matching (supports wildcards)
|
||||||
|
* @param {Array} workflows - Array of workflow objects
|
||||||
|
* @param {Array} selectedWorkflows - Array of workflow names to filter by (supports * wildcard)
|
||||||
|
* @returns {Array} Filtered workflows
|
||||||
|
*/
|
||||||
|
filterWorkflows(workflows, selectedWorkflows) {
|
||||||
|
if (!selectedWorkflows || selectedWorkflows.length === 0) {
|
||||||
|
return workflows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for special values
|
||||||
|
if (selectedWorkflows.includes('all')) {
|
||||||
|
return workflows;
|
||||||
|
}
|
||||||
|
if (selectedWorkflows.includes('none')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return workflows.filter((workflow) => {
|
||||||
|
const workflowName = workflow.name;
|
||||||
|
|
||||||
|
return selectedWorkflows.some((pattern) => {
|
||||||
|
// Exact match
|
||||||
|
if (pattern === workflowName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard matching: create-* matches create-prd, create-tech-spec, etc.
|
||||||
|
if (pattern.includes('*')) {
|
||||||
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
||||||
|
return regex.test(workflowName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -197,8 +252,10 @@ class ManifestGenerator {
|
||||||
/**
|
/**
|
||||||
* Collect all agents from core and selected modules
|
* Collect all agents from core and selected modules
|
||||||
* Scans the INSTALLED bmad directory, not the source
|
* Scans the INSTALLED bmad directory, not the source
|
||||||
|
* @param {Array} selectedModules - Modules to scan for agents
|
||||||
|
* @param {Array|null} selectedAgents - Optional array of agent names to filter by
|
||||||
*/
|
*/
|
||||||
async collectAgents(selectedModules) {
|
async collectAgents(selectedModules, selectedAgents = null) {
|
||||||
this.agents = [];
|
this.agents = [];
|
||||||
|
|
||||||
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
||||||
|
|
@ -224,6 +281,50 @@ class ManifestGenerator {
|
||||||
this.agents.push(...standaloneAgents);
|
this.agents.push(...standaloneAgents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply filtering if selectedAgents is provided
|
||||||
|
if (selectedAgents && Array.isArray(selectedAgents) && selectedAgents.length > 0) {
|
||||||
|
this.agents = this.filterAgents(this.agents, selectedAgents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter agents by name matching (supports wildcards)
|
||||||
|
* @param {Array} agents - Array of agent objects
|
||||||
|
* @param {Array} selectedAgents - Array of agent names to filter by (supports * wildcard)
|
||||||
|
* @returns {Array} Filtered agents
|
||||||
|
*/
|
||||||
|
filterAgents(agents, selectedAgents) {
|
||||||
|
if (!selectedAgents || selectedAgents.length === 0) {
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for special values
|
||||||
|
if (selectedAgents.includes('all')) {
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
if (selectedAgents.includes('none')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return agents.filter((agent) => {
|
||||||
|
const agentName = agent.name;
|
||||||
|
|
||||||
|
return selectedAgents.some((pattern) => {
|
||||||
|
// Exact match
|
||||||
|
if (pattern === agentName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard matching: dev* matches dev, dev-story, etc.
|
||||||
|
if (pattern.includes('*')) {
|
||||||
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
||||||
|
return regex.test(agentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -462,11 +563,20 @@ class ManifestGenerator {
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
installDate: new Date().toISOString(),
|
installDate: new Date().toISOString(),
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
|
installMode: this.installMode || 'interactive',
|
||||||
},
|
},
|
||||||
modules: this.modules, // Include ALL modules (standard and custom)
|
modules: this.modules, // Include ALL modules (standard and custom)
|
||||||
ides: this.selectedIdes,
|
ides: this.selectedIdes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add selective installation info if filters were applied
|
||||||
|
if (this.selectedAgentsList && this.selectedAgentsList.length > 0) {
|
||||||
|
manifest.selectedAgents = this.selectedAgentsList;
|
||||||
|
}
|
||||||
|
if (this.selectedWorkflowsList && this.selectedWorkflowsList.length > 0) {
|
||||||
|
manifest.selectedWorkflows = this.selectedWorkflowsList;
|
||||||
|
}
|
||||||
|
|
||||||
// Clean the manifest to remove any non-serializable values
|
// Clean the manifest to remove any non-serializable values
|
||||||
const cleanManifest = structuredClone(manifest);
|
const cleanManifest = structuredClone(manifest);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -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 {
|
class UI {
|
||||||
/**
|
/**
|
||||||
* Prompt for installation configuration
|
* Prompt for installation configuration
|
||||||
|
* @param {Object} cliOptions - CLI options for non-interactive mode
|
||||||
* @returns {Object} Installation configuration
|
* @returns {Object} Installation configuration
|
||||||
*/
|
*/
|
||||||
async promptInstall() {
|
async promptInstall(cliOptions = {}) {
|
||||||
|
// Handle non-interactive mode
|
||||||
|
if (cliOptions.nonInteractive) {
|
||||||
|
return await this.buildNonInteractiveConfig(cliOptions);
|
||||||
|
}
|
||||||
|
|
||||||
CLIUtils.displayLogo();
|
CLIUtils.displayLogo();
|
||||||
|
|
||||||
// Display changelog link
|
// Display changelog link
|
||||||
|
|
@ -1457,6 +1463,139 @@ class UI {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build non-interactive installation configuration
|
||||||
|
* @param {Object} cliOptions - CLI options
|
||||||
|
* @returns {Object} Installation configuration
|
||||||
|
*/
|
||||||
|
async buildNonInteractiveConfig(cliOptions) {
|
||||||
|
const { parseOptions } = require('../installers/lib/core/options-parser');
|
||||||
|
const { expandTeam, applyTeamModifiers } = require('../installers/lib/teams/team-loader');
|
||||||
|
const { getProjectRoot } = require('./project-root');
|
||||||
|
const { getEnvironmentDefaults } = require('../installers/lib/core/env-resolver');
|
||||||
|
|
||||||
|
console.log(chalk.cyan('🤖 Running non-interactive installation...\n'));
|
||||||
|
|
||||||
|
// Parse and normalize options
|
||||||
|
const options = parseOptions(cliOptions);
|
||||||
|
const envDefaults = getEnvironmentDefaults();
|
||||||
|
|
||||||
|
// Determine directory
|
||||||
|
const directory = process.cwd();
|
||||||
|
|
||||||
|
// Check for existing installation
|
||||||
|
const { Installer } = require('../installers/lib/core/installer');
|
||||||
|
const installer = new Installer();
|
||||||
|
const { bmadDir, hasExistingInstall } = await installer.findBmadDir(directory);
|
||||||
|
const actionType = hasExistingInstall ? 'update' : 'install';
|
||||||
|
|
||||||
|
console.log(chalk.dim(` Directory: ${directory}`));
|
||||||
|
console.log(chalk.dim(` Action: ${actionType === 'install' ? 'New installation' : 'Update existing installation'}\n`));
|
||||||
|
|
||||||
|
// Determine modules to install
|
||||||
|
let selectedModules = [];
|
||||||
|
|
||||||
|
if (options.team) {
|
||||||
|
// Team-based installation
|
||||||
|
console.log(chalk.cyan(`📦 Loading team: ${options.team}...`));
|
||||||
|
try {
|
||||||
|
const projectRoot = getProjectRoot();
|
||||||
|
let teamExpansion = await expandTeam(options.team, projectRoot);
|
||||||
|
|
||||||
|
// Apply modifiers if present
|
||||||
|
if (options.agents || options.workflows) {
|
||||||
|
const { separateModifiers } = require('../installers/lib/core/options-parser');
|
||||||
|
const agentMods = options.agents ? separateModifiers(options.agents) : { base: [], add: [], remove: [] };
|
||||||
|
const workflowMods = options.workflows ? separateModifiers(options.workflows) : { base: [], add: [], remove: [] };
|
||||||
|
|
||||||
|
// If base is provided, replace team selections completely
|
||||||
|
if (agentMods.base.length > 0) {
|
||||||
|
teamExpansion.agents = agentMods.base;
|
||||||
|
}
|
||||||
|
if (workflowMods.base.length > 0) {
|
||||||
|
teamExpansion.workflows = workflowMods.base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply modifiers
|
||||||
|
teamExpansion = applyTeamModifiers(
|
||||||
|
teamExpansion,
|
||||||
|
[...agentMods.add.map((a) => `+${a}`), ...agentMods.remove.map((a) => `-${a}`)],
|
||||||
|
[...workflowMods.add.map((w) => `+${w}`), ...workflowMods.remove.map((w) => `-${w}`)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.agents = teamExpansion.agents;
|
||||||
|
options.workflows = teamExpansion.workflows;
|
||||||
|
|
||||||
|
// Determine module from team
|
||||||
|
if (teamExpansion.module && !selectedModules.includes(teamExpansion.module)) {
|
||||||
|
selectedModules.push(teamExpansion.module);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.green(` ✓ Team loaded: ${options.team}`));
|
||||||
|
console.log(chalk.dim(` Agents: ${teamExpansion.agents.join(', ')}`));
|
||||||
|
if (teamExpansion.workflows && teamExpansion.workflows.length > 0) {
|
||||||
|
console.log(chalk.dim(` Workflows: ${teamExpansion.workflows.join(', ')}`));
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(chalk.red(` ✗ Failed to load team: ${error.message}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else if (options.modules) {
|
||||||
|
// Module-based installation
|
||||||
|
if (options.modules === 'all') {
|
||||||
|
selectedModules = ['bmm', 'bmbb', 'cis', 'bmgd'];
|
||||||
|
} else if (Array.isArray(options.modules)) {
|
||||||
|
selectedModules = options.modules.filter((m) => m !== 'core');
|
||||||
|
}
|
||||||
|
} else if (options.profile) {
|
||||||
|
// Profile-based installation
|
||||||
|
const { getProfile } = require('../installers/lib/profiles/definitions');
|
||||||
|
const profile = getProfile(options.profile);
|
||||||
|
if (profile.modules === 'all') {
|
||||||
|
selectedModules = ['bmm', 'bmbb', 'cis', 'bmgd'];
|
||||||
|
} else if (Array.isArray(profile.modules)) {
|
||||||
|
selectedModules = profile.modules.filter((m) => m !== 'core');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default: install bmm
|
||||||
|
selectedModules = ['bmm'];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.cyan(`📦 Modules: ${selectedModules.length > 0 ? selectedModules.join(', ') : 'core only'}\n`));
|
||||||
|
|
||||||
|
// Build core configuration
|
||||||
|
const coreConfig = {
|
||||||
|
user_name: options.userName || envDefaults.userName,
|
||||||
|
user_skill_level: options.skillLevel || 'intermediate',
|
||||||
|
communication_language: options.communicationLanguage || envDefaults.communicationLanguage,
|
||||||
|
document_output_language: options.documentLanguage || envDefaults.documentLanguage,
|
||||||
|
output_folder: options.outputFolder || '_bmad-output',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(chalk.cyan('⚙️ Configuration:'));
|
||||||
|
console.log(chalk.dim(` User: ${coreConfig.user_name}`));
|
||||||
|
console.log(chalk.dim(` Skill Level: ${coreConfig.user_skill_level}`));
|
||||||
|
console.log(chalk.dim(` Language: ${coreConfig.communication_language}\n`));
|
||||||
|
|
||||||
|
// Return installation configuration
|
||||||
|
return {
|
||||||
|
actionType,
|
||||||
|
directory,
|
||||||
|
installCore: true,
|
||||||
|
modules: selectedModules,
|
||||||
|
ides: ['claude-code'], // Default to Claude Code for non-interactive
|
||||||
|
skipIde: false,
|
||||||
|
coreConfig,
|
||||||
|
customContent: { hasCustomContent: false },
|
||||||
|
enableAgentVibes: false,
|
||||||
|
agentVibesInstalled: false,
|
||||||
|
// Pass through CLI options for downstream use
|
||||||
|
cliOptions: options,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { UI };
|
module.exports = { UI };
|
||||||
|
|
|
||||||
|
|
@ -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