From b6ade22984d1e4a37c0016a925031a4e9e1c71da Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Wed, 17 Dec 2025 11:37:07 +0200 Subject: [PATCH 1/8] feat: introduce non-interactive cli installation --- README.md | 61 +++ docs/non-interactive-installation-guide.md | 440 ++++++++++++++++++ tools/cli/commands/install.js | 39 +- .../installers/lib/core/config-collector.js | 175 +++++++ tools/cli/installers/lib/core/env-resolver.js | 120 +++++ tools/cli/installers/lib/core/installer.js | 3 + .../installers/lib/core/manifest-generator.js | 118 ++++- .../cli/installers/lib/core/options-parser.js | 198 ++++++++ .../installers/lib/profiles/definitions.js | 103 ++++ tools/cli/installers/lib/teams/team-loader.js | 181 +++++++ tools/cli/lib/ui.js | 141 +++++- tools/cli/test/test-non-interactive.sh | 178 +++++++ 12 files changed, 1749 insertions(+), 8 deletions(-) create mode 100644 docs/non-interactive-installation-guide.md create mode 100644 tools/cli/installers/lib/core/env-resolver.js create mode 100644 tools/cli/installers/lib/core/options-parser.js create mode 100644 tools/cli/installers/lib/profiles/definitions.js create mode 100644 tools/cli/installers/lib/teams/team-loader.js create mode 100755 tools/cli/test/test-non-interactive.sh diff --git a/README.md b/README.md index 7a271e6d..e23f6bb6 100644 --- a/README.md +++ b/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 ` - User name for configuration +- `--skill-level ` - beginner, intermediate, advanced +- `--output-folder ` - Output folder for BMAD artifacts +- `--modules ` - Comma-separated module list +- `--agents ` - Comma-separated agent list or 'all', 'none' +- `--workflows ` - Comma-separated workflow list or 'all', 'none' +- `--team ` - Install predefined team (fullstack, gamedev) +- `--profile ` - 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: diff --git a/docs/non-interactive-installation-guide.md b/docs/non-interactive-installation-guide.md new file mode 100644 index 00000000..dd004702 --- /dev/null +++ b/docs/non-interactive-installation-guide.md @@ -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 ` | User name | System user | `--user-name=Alice` | +| `--skill-level ` | Skill level | `intermediate` | `--skill-level=advanced` | +| `--output-folder ` | Output folder | `_bmad-output` | `--output-folder=.artifacts` | +| `--communication-language ` | Communication language | `English` | `--communication-language=Spanish` | +| `--document-language ` | Document language | `English` | `--document-language=French` | + +### Module & Selection Options + +| Option | Description | Example | +|--------|-------------|---------| +| `--modules ` | Comma-separated modules | `--modules=core,bmm,bmbb` | +| `--agents ` | Comma-separated agents | `--agents=dev,architect,pm` | +| `--workflows ` | Comma-separated workflows | `--workflows=create-prd,dev-story` | + +### Team & Profile Options + +| Option | Description | Example | +|--------|-------------|---------| +| `--team ` | Install predefined team | `--team=fullstack` | +| `--profile ` | 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 diff --git a/tools/cli/commands/install.js b/tools/cli/commands/install.js index 42e0afb8..01a5a140 100644 --- a/tools/cli/commands/install.js +++ b/tools/cli/commands/install.js @@ -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 ', 'User name for configuration'], + ['--skill-level ', 'User skill level (beginner, intermediate, advanced)'], + ['--output-folder ', 'Output folder path for BMAD artifacts'], + ['--modules ', 'Comma-separated list of modules to install (e.g., core,bmm)'], + ['--agents ', 'Comma-separated list of agents to install (e.g., dev,architect,pm)'], + ['--workflows ', 'Comma-separated list of workflows to install'], + ['--team ', 'Install predefined team bundle (e.g., fullstack, gamedev)'], + ['--profile ', 'Installation profile (minimal, full, solo-dev)'], + ['--communication-language ', 'Language for agent communication (default: English)'], + ['--document-language ', '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') { diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index 5898ace5..47a155bd 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -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 diff --git a/tools/cli/installers/lib/core/env-resolver.js b/tools/cli/installers/lib/core/env-resolver.js new file mode 100644 index 00000000..12fce4df --- /dev/null +++ b/tools/cli/installers/lib/core/env-resolver.js @@ -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, +}; diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 37334348..5ff81ff0 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -1075,6 +1075,9 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: 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 diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 2de9c2cf..f01d46ca 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -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); diff --git a/tools/cli/installers/lib/core/options-parser.js b/tools/cli/installers/lib/core/options-parser.js new file mode 100644 index 00000000..7c159d68 --- /dev/null +++ b/tools/cli/installers/lib/core/options-parser.js @@ -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, +}; diff --git a/tools/cli/installers/lib/profiles/definitions.js b/tools/cli/installers/lib/profiles/definitions.js new file mode 100644 index 00000000..0a0b0dcd --- /dev/null +++ b/tools/cli/installers/lib/profiles/definitions.js @@ -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= + * 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, +}; diff --git a/tools/cli/installers/lib/teams/team-loader.js b/tools/cli/installers/lib/teams/team-loader.js new file mode 100644 index 00000000..9a2403df --- /dev/null +++ b/tools/cli/installers/lib/teams/team-loader.js @@ -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} 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} 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} { 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} 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, +}; diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index d3dfcafc..81af5569 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -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 @@ -1439,6 +1445,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 }; diff --git a/tools/cli/test/test-non-interactive.sh b/tools/cli/test/test-non-interactive.sh new file mode 100755 index 00000000..cf3071be --- /dev/null +++ b/tools/cli/test/test-non-interactive.sh @@ -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 From 1dca5fb4b739068976bb049304fcb0e599b9cbae Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Thu, 18 Dec 2025 12:23:46 +0200 Subject: [PATCH 2/8] fix: lint and formatting issues in non-interactive installation PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix ESLint errors: use switch over else-if, remove unused catch bindings, use slice over substring, use replaceAll over replace - Fix Prettier formatting issues across all modified files - Fix markdown lint: wrap bare URL in angle brackets All tests passing: schemas, installation, validation, lint, markdown lint, and formatting checks. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/non-interactive-installation-guide.md | 44 ++++++++++------- .../installers/lib/core/config-collector.js | 47 ++++++++++++------- tools/cli/installers/lib/core/env-resolver.js | 2 +- .../installers/lib/core/manifest-generator.js | 4 +- .../cli/installers/lib/core/options-parser.js | 20 +++----- .../installers/lib/profiles/definitions.js | 9 +--- tools/cli/installers/lib/teams/team-loader.js | 14 +++--- 7 files changed, 73 insertions(+), 67 deletions(-) diff --git a/docs/non-interactive-installation-guide.md b/docs/non-interactive-installation-guide.md index dd004702..97a27ea3 100644 --- a/docs/non-interactive-installation-guide.md +++ b/docs/non-interactive-installation-guide.md @@ -21,6 +21,7 @@ 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`) @@ -49,10 +50,11 @@ cat _bmad/bmm/config.yaml ``` Example output: + ```yaml user_name: Alice user_skill_level: intermediate -output_folder: "{project-root}/_bmad-output" +output_folder: '{project-root}/_bmad-output' communication_language: English ``` @@ -169,29 +171,29 @@ npx bmad-method@alpha install -y \ ### Core Options -| Option | Description | Default | Example | -|--------|-------------|---------|---------| -| `-y, --non-interactive` | Skip all prompts | `false` | `install -y` | -| `--user-name ` | User name | System user | `--user-name=Alice` | -| `--skill-level ` | Skill level | `intermediate` | `--skill-level=advanced` | -| `--output-folder ` | Output folder | `_bmad-output` | `--output-folder=.artifacts` | -| `--communication-language ` | Communication language | `English` | `--communication-language=Spanish` | -| `--document-language ` | Document language | `English` | `--document-language=French` | +| Option | Description | Default | Example | +| --------------------------------- | ---------------------- | -------------- | ---------------------------------- | +| `-y, --non-interactive` | Skip all prompts | `false` | `install -y` | +| `--user-name ` | User name | System user | `--user-name=Alice` | +| `--skill-level ` | Skill level | `intermediate` | `--skill-level=advanced` | +| `--output-folder ` | Output folder | `_bmad-output` | `--output-folder=.artifacts` | +| `--communication-language ` | Communication language | `English` | `--communication-language=Spanish` | +| `--document-language ` | Document language | `English` | `--document-language=French` | ### Module & Selection Options -| Option | Description | Example | -|--------|-------------|---------| -| `--modules ` | Comma-separated modules | `--modules=core,bmm,bmbb` | -| `--agents ` | Comma-separated agents | `--agents=dev,architect,pm` | +| Option | Description | Example | +| -------------------- | ------------------------- | ---------------------------------- | +| `--modules ` | Comma-separated modules | `--modules=core,bmm,bmbb` | +| `--agents ` | Comma-separated agents | `--agents=dev,architect,pm` | | `--workflows ` | Comma-separated workflows | `--workflows=create-prd,dev-story` | ### Team & Profile Options -| Option | Description | Example | -|--------|-------------|---------| -| `--team ` | Install predefined team | `--team=fullstack` | -| `--profile ` | Installation profile | `--profile=minimal` | +| Option | Description | Example | +| ------------------ | ----------------------- | ------------------- | +| `--team ` | Install predefined team | `--team=fullstack` | +| `--profile ` | Installation profile | `--profile=minimal` | ## Team-Based Installation @@ -206,6 +208,7 @@ npx bmad-method@alpha install -y --team=fullstack ``` **Includes:** + - Agents: analyst, architect, pm, sm, ux-designer - Module: BMM @@ -218,6 +221,7 @@ 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) @@ -252,6 +256,7 @@ npx bmad-method@alpha install -y --profile=minimal ``` **Includes:** + - Modules: core - Agents: dev - Workflows: create-tech-spec, quick-dev @@ -265,6 +270,7 @@ 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 @@ -278,6 +284,7 @@ npx bmad-method@alpha install -y --profile=full ``` **Includes:** + - All modules - All agents - All workflows @@ -291,6 +298,7 @@ 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 @@ -437,4 +445,4 @@ fi ## Feedback Found an issue or have a suggestion? Please report it at: -https://github.com/bmad-code-org/BMAD-METHOD/issues + diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index 47a155bd..d7e8d3c3 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -885,22 +885,37 @@ class ConfigCollector { // 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; + switch (key) { + case 'user_name': { + value = resolveValue(cliOptions.userName, null, envDefaults.userName); + + break; + } + case 'user_skill_level': { + value = resolveValue(cliOptions.skillLevel, null, item.default || 'intermediate'); + + break; + } + case 'communication_language': { + value = resolveValue(cliOptions.communicationLanguage, null, envDefaults.communicationLanguage); + + break; + } + case 'document_output_language': { + value = resolveValue(cliOptions.documentLanguage, null, envDefaults.documentLanguage); + + break; + } + case 'output_folder': { + value = resolveValue(cliOptions.outputFolder, null, item.default); + + break; + } + default: { + if (item.default !== undefined) { + value = item.default; + } + } } } else { // For other modules, use defaults diff --git a/tools/cli/installers/lib/core/env-resolver.js b/tools/cli/installers/lib/core/env-resolver.js index 12fce4df..8a0d5171 100644 --- a/tools/cli/installers/lib/core/env-resolver.js +++ b/tools/cli/installers/lib/core/env-resolver.js @@ -26,7 +26,7 @@ function getUserName() { if (userInfo.username) { return userInfo.username; } - } catch (error) { + } catch { // os.userInfo() can fail in some environments } diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index f01d46ca..07e521c3 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -161,7 +161,7 @@ class ManifestGenerator { // Wildcard matching: create-* matches create-prd, create-tech-spec, etc. if (pattern.includes('*')) { - const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$'); + const regex = new RegExp('^' + pattern.replaceAll('*', '.*') + '$'); return regex.test(workflowName); } @@ -318,7 +318,7 @@ class ManifestGenerator { // Wildcard matching: dev* matches dev, dev-story, etc. if (pattern.includes('*')) { - const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$'); + const regex = new RegExp('^' + pattern.replaceAll('*', '.*') + '$'); return regex.test(agentName); } diff --git a/tools/cli/installers/lib/core/options-parser.js b/tools/cli/installers/lib/core/options-parser.js index 7c159d68..660c4e6f 100644 --- a/tools/cli/installers/lib/core/options-parser.js +++ b/tools/cli/installers/lib/core/options-parser.js @@ -58,9 +58,9 @@ function separateModifiers(list) { for (const item of list) { if (item.startsWith('+')) { - result.add.push(item.substring(1)); + result.add.push(item.slice(1)); } else if (item.startsWith('-')) { - result.remove.push(item.substring(1)); + result.remove.push(item.slice(1)); } else { result.base.push(item); } @@ -118,9 +118,7 @@ function parseOptions(cliOptions) { if (normalized.profile) { const profile = getProfile(normalized.profile); if (!profile) { - throw new Error( - `Unknown profile: ${normalized.profile}. Valid profiles: minimal, full, solo-dev, team`, - ); + throw new Error(`Unknown profile: ${normalized.profile}. Valid profiles: minimal, full, solo-dev, team`); } // Profile provides defaults, but CLI options override @@ -136,9 +134,7 @@ function parseOptions(cliOptions) { normalized.agents = Array.isArray(profile.agents) ? profile.agents : profile.agents; } if (!normalized.workflows) { - normalized.workflows = Array.isArray(profile.workflows) - ? profile.workflows - : profile.workflows; + normalized.workflows = Array.isArray(profile.workflows) ? profile.workflows : profile.workflows; } } @@ -157,9 +153,7 @@ function validateOptions(options) { 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`, - ); + errors.push(`Invalid skill level: ${options.skillLevel}. Valid values: beginner, intermediate, advanced`); } } @@ -167,9 +161,7 @@ function validateOptions(options) { 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`, - ); + errors.push(`Invalid profile: ${options.profile}. Valid values: minimal, full, solo-dev, team`); } } diff --git a/tools/cli/installers/lib/profiles/definitions.js b/tools/cli/installers/lib/profiles/definitions.js index 0a0b0dcd..9109cd91 100644 --- a/tools/cli/installers/lib/profiles/definitions.js +++ b/tools/cli/installers/lib/profiles/definitions.js @@ -28,14 +28,7 @@ const PROFILES = { 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', - ], + workflows: ['create-tech-spec', 'quick-dev', 'dev-story', 'code-review', 'create-prd', 'create-architecture'], }, team: { diff --git a/tools/cli/installers/lib/teams/team-loader.js b/tools/cli/installers/lib/teams/team-loader.js index 9a2403df..0c2c032d 100644 --- a/tools/cli/installers/lib/teams/team-loader.js +++ b/tools/cli/installers/lib/teams/team-loader.js @@ -49,7 +49,7 @@ async function discoverTeams(projectRoot) { } return teams; - } catch (error) { + } catch { // If glob fails, return empty array return []; } @@ -75,9 +75,7 @@ async function loadTeam(teamName, projectRoot) { 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'}`, - ); + throw new Error(`Team '${teamName}' not found. Available teams: ${availableTeams || 'none'}`); } // Load full team definition @@ -130,12 +128,12 @@ function applyTeamModifiers(team, agentModifiers = [], workflowModifiers = []) { // Parse and apply agent modifiers for (const modifier of agentModifiers) { if (modifier.startsWith('+')) { - const agent = modifier.substring(1); + const agent = modifier.slice(1); if (!result.agents.includes(agent)) { result.agents.push(agent); } } else if (modifier.startsWith('-')) { - const agent = modifier.substring(1); + const agent = modifier.slice(1); result.agents = result.agents.filter((a) => a !== agent); } } @@ -143,12 +141,12 @@ function applyTeamModifiers(team, agentModifiers = [], workflowModifiers = []) { // Parse and apply workflow modifiers for (const modifier of workflowModifiers) { if (modifier.startsWith('+')) { - const workflow = modifier.substring(1); + const workflow = modifier.slice(1); if (!result.workflows.includes(workflow)) { result.workflows.push(workflow); } } else if (modifier.startsWith('-')) { - const workflow = modifier.substring(1); + const workflow = modifier.slice(1); result.workflows = result.workflows.filter((w) => w !== workflow); } } From b88845c0fe80d3a18a8aafd1a97aa86f74489107 Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Thu, 18 Dec 2025 12:32:04 +0200 Subject: [PATCH 3/8] fix: remove useless ternary operators in options-parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ternary operators were checking `Array.isArray()` but returning the same value in both branches, making them completely pointless. Since profiles can contain both arrays (e.g., `['dev', 'architect']`) and strings (e.g., `'all'`), and both are valid, we should just assign the value directly. Fixed lines: - normalized.modules = profile.modules - normalized.agents = profile.agents - normalized.workflows = profile.workflows ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tools/cli/installers/lib/core/options-parser.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/cli/installers/lib/core/options-parser.js b/tools/cli/installers/lib/core/options-parser.js index 660c4e6f..2e69ce47 100644 --- a/tools/cli/installers/lib/core/options-parser.js +++ b/tools/cli/installers/lib/core/options-parser.js @@ -128,13 +128,13 @@ function parseOptions(cliOptions) { // If no explicit modules/agents/workflows, use profile values if (!normalized.modules) { - normalized.modules = Array.isArray(profile.modules) ? profile.modules : profile.modules; + normalized.modules = profile.modules; } if (!normalized.agents) { - normalized.agents = Array.isArray(profile.agents) ? profile.agents : profile.agents; + normalized.agents = profile.agents; } if (!normalized.workflows) { - normalized.workflows = Array.isArray(profile.workflows) ? profile.workflows : profile.workflows; + normalized.workflows = profile.workflows; } } From e9c0aafad91184719f5044606a667e761e287e44 Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Thu, 18 Dec 2025 12:36:30 +0200 Subject: [PATCH 4/8] fix: wrap string profile values in arrays for consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ternary operators WERE needed after all! Profile values can be: - Arrays: ['dev', 'architect', 'pm'] - Strings: 'all' (special keyword) Downstream code expects arrays: - filterWorkflows() checks selectedWorkflows.includes('all') - filterAgents() checks selectedAgents.includes('all') - separateModifiers() iterates with for-of loop Without wrapping strings in arrays: - 'all' โ†’ stays as string โ†’ includes() doesn't work - WITH fix: 'all' โ†’ becomes ['all'] โ†’ includes('all') works โœ“ This fixes the profile workflow: 1. Profile defines: workflows: 'all' 2. Parser wraps: normalized.workflows = ['all'] 3. Filter checks: selectedWorkflows.includes('all') โ†’ true โœ“ ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tools/cli/installers/lib/core/options-parser.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/cli/installers/lib/core/options-parser.js b/tools/cli/installers/lib/core/options-parser.js index 2e69ce47..36bb110b 100644 --- a/tools/cli/installers/lib/core/options-parser.js +++ b/tools/cli/installers/lib/core/options-parser.js @@ -127,14 +127,15 @@ function parseOptions(cliOptions) { normalized.profileWorkflows = profile.workflows; // If no explicit modules/agents/workflows, use profile values + // Ensure strings like 'all' are wrapped in arrays for consistency if (!normalized.modules) { - normalized.modules = profile.modules; + normalized.modules = Array.isArray(profile.modules) ? profile.modules : [profile.modules]; } if (!normalized.agents) { - normalized.agents = profile.agents; + normalized.agents = Array.isArray(profile.agents) ? profile.agents : [profile.agents]; } if (!normalized.workflows) { - normalized.workflows = profile.workflows; + normalized.workflows = Array.isArray(profile.workflows) ? profile.workflows : [profile.workflows]; } } From 32b104b4bc3b3fd6d136d54abe0f203fdd871578 Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Thu, 18 Dec 2025 12:39:07 +0200 Subject: [PATCH 5/8] fix: handle array-wrapped 'all' value for modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After wrapping profile string values in arrays in options-parser, the modules handling in ui.js was still only checking for string 'all', not array ['all']. This would break profiles with `modules: 'all'`. Added checks for both cases: - String: `options.modules === 'all'` (original case) - Array: `options.modules.includes('all')` (new case after wrapping) Now modules handling is consistent with agents/workflows filtering which already used `.includes('all')`. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tools/cli/lib/ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 714daed2..d0e12b0b 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -1545,7 +1545,7 @@ class UI { } } else if (options.modules) { // Module-based installation - if (options.modules === 'all') { + if (options.modules === 'all' || (Array.isArray(options.modules) && options.modules.includes('all'))) { selectedModules = ['bmm', 'bmbb', 'cis', 'bmgd']; } else if (Array.isArray(options.modules)) { selectedModules = options.modules.filter((m) => m !== 'core'); @@ -1554,7 +1554,7 @@ class UI { // Profile-based installation const { getProfile } = require('../installers/lib/profiles/definitions'); const profile = getProfile(options.profile); - if (profile.modules === 'all') { + if (profile.modules === 'all' || (Array.isArray(profile.modules) && profile.modules.includes('all'))) { selectedModules = ['bmm', 'bmbb', 'cis', 'bmgd']; } else if (Array.isArray(profile.modules)) { selectedModules = profile.modules.filter((m) => m !== 'core'); From 133181aaa9f644ed2d8fe3935d32409c1f8811d5 Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Thu, 18 Dec 2025 12:42:53 +0200 Subject: [PATCH 6/8] chore: add .idea/ to .gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added IntelliJ IDEA project configuration directory to gitignore to prevent IDE-specific files from being committed. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3c8f80c7..50af62bf 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ Thumbs.db .prettierrc # IDE and editor configs +.idea/ .windsurf/ .trae/ _bmad*/.cursor/ From 3a663a9a924a06c67e713465c2593df6edbbabc8 Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Thu, 18 Dec 2025 12:43:53 +0200 Subject: [PATCH 7/8] chore: add _bmad-output/ to .gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added BMAD output directory to gitignore to prevent generated output files from being committed. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 50af62bf..719df996 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ shared-modules z*/ _bmad +_bmad-output/ .claude .codex .github/chatmodes From 251ec93150000096ed163a1311373510a76332b8 Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Thu, 18 Dec 2025 12:59:22 +0200 Subject: [PATCH 8/8] refactor: remove dead code (profileModules/Agents/Workflows) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed unused properties that were set but never read: - normalized.profileModules - normalized.profileAgents - normalized.profileWorkflows These values were: 1. Stored as unwrapped (strings like 'all' or arrays) 2. Never accessed anywhere in the codebase 3. Created confusion - actual values used are normalized.modules/agents/workflows 4. Inconsistent with the wrapped versions actually used downstream The profile values are already correctly processed and stored in normalized.modules/agents/workflows (with proper array wrapping), which are then passed to installer via config.cliOptions. No functional change - just removing dead code that served no purpose. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tools/cli/installers/lib/core/options-parser.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tools/cli/installers/lib/core/options-parser.js b/tools/cli/installers/lib/core/options-parser.js index 36bb110b..036ee6a0 100644 --- a/tools/cli/installers/lib/core/options-parser.js +++ b/tools/cli/installers/lib/core/options-parser.js @@ -121,12 +121,7 @@ function parseOptions(cliOptions) { 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 + // Use profile values as defaults when CLI options not provided // Ensure strings like 'all' are wrapped in arrays for consistency if (!normalized.modules) { normalized.modules = Array.isArray(profile.modules) ? profile.modules : [profile.modules];