This commit is contained in:
Your Name 2026-02-09 11:37:55 +08:00
commit 892db62df2
96 changed files with 3909 additions and 3976 deletions

View File

@ -0,0 +1,271 @@
# Augment Code Review Guidelines for BMAD-METHOD
# https://docs.augmentcode.com/codereview/overview
# Focus: Workflow validation and quality
file_paths_to_ignore:
# --- Shared baseline: tool configs ---
- ".coderabbit.yaml"
- ".augment/**"
- "eslint.config.mjs"
# --- Shared baseline: build output ---
- "dist/**"
- "build/**"
- "coverage/**"
# --- Shared baseline: vendored/generated ---
- "node_modules/**"
- "**/*.min.js"
- "**/*.generated.*"
- "**/*.bundle.md"
# --- Shared baseline: package metadata ---
- "package-lock.json"
# --- Shared baseline: binary/media ---
- "*.png"
- "*.jpg"
- "*.svg"
# --- Shared baseline: test fixtures ---
- "test/fixtures/**"
- "test/template-test-generator/**"
- "tools/template-test-generator/test-scenarios/**"
# --- Shared baseline: non-project dirs ---
- "_bmad*/**"
- "website/**"
- "z*/**"
- "sample-project/**"
- "test-project-install/**"
# --- Shared baseline: AI assistant dirs ---
- ".claude/**"
- ".codex/**"
- ".agent/**"
- ".agentvibes/**"
- ".kiro/**"
- ".roo/**"
- ".github/chatmodes/**"
# --- Shared baseline: build temp ---
- ".bundler-temp/**"
# --- Shared baseline: generated reports ---
- "**/validation-report-*.md"
- "CHANGELOG.md"
areas:
# ============================================
# WORKFLOW STRUCTURE RULES
# ============================================
workflow_structure:
description: "Workflow folder organization and required components"
globs:
- "src/**/workflows/**"
rules:
- id: "workflow_entry_point_required"
description: "Every workflow folder must have workflow.yaml, workflow.md, or workflow.xml as entry point"
severity: "high"
- id: "sharded_workflow_steps_folder"
description: "Sharded workflows (using workflow.md) must have steps/ folder with numbered files (step-01-*.md, step-02-*.md)"
severity: "high"
- id: "standard_workflow_instructions"
description: "Standard workflows using workflow.yaml must include instructions.md for execution guidance"
severity: "medium"
- id: "workflow_step_limit"
description: "Workflows should have 5-10 steps maximum to prevent context loss in LLM execution"
severity: "medium"
# ============================================
# WORKFLOW ENTRY FILE RULES
# ============================================
workflow_definitions:
description: "Workflow entry files (workflow.yaml, workflow.md, workflow.xml)"
globs:
- "src/**/workflows/**/workflow.yaml"
- "src/**/workflows/**/workflow.md"
- "src/**/workflows/**/workflow.xml"
rules:
- id: "workflow_name_required"
description: "Workflow entry files must define 'name' field in frontmatter or root element"
severity: "high"
- id: "workflow_description_required"
description: "Workflow entry files must include 'description' explaining the workflow's purpose"
severity: "high"
- id: "workflow_config_source"
description: "Workflows should reference config_source for variable resolution (e.g., {project-root}/_bmad/module/config.yaml)"
severity: "medium"
- id: "workflow_installed_path"
description: "Workflows should define installed_path for relative file references within the workflow"
severity: "medium"
- id: "valid_step_references"
description: "Step file references in workflow entry must point to existing files"
severity: "high"
# ============================================
# SHARDED WORKFLOW STEP RULES
# ============================================
workflow_steps:
description: "Individual step files in sharded workflows"
globs:
- "src/**/workflows/**/steps/step-*.md"
rules:
- id: "step_goal_required"
description: "Each step must clearly state its goal (## STEP GOAL, ## YOUR TASK, or step n='X' goal='...')"
severity: "high"
- id: "step_mandatory_rules"
description: "Step files should include MANDATORY EXECUTION RULES section with universal agent behavior rules"
severity: "medium"
- id: "step_context_boundaries"
description: "Step files should define CONTEXT BOUNDARIES explaining available context and limits"
severity: "medium"
- id: "step_success_metrics"
description: "Step files should include SUCCESS METRICS section with ✅ checkmarks for validation criteria"
severity: "medium"
- id: "step_failure_modes"
description: "Step files should include FAILURE MODES section with ❌ marks for anti-patterns to avoid"
severity: "medium"
- id: "step_next_step_reference"
description: "Step files should reference the next step file path for sequential execution"
severity: "medium"
- id: "step_no_forward_loading"
description: "Steps must NOT load future step files until current step completes - just-in-time loading only"
severity: "high"
- id: "valid_file_references"
description: "File path references using {variable}/filename.md must point to existing files"
severity: "high"
- id: "step_naming"
description: "Step files must be named step-NN-description.md (e.g., step-01-init.md, step-02-context.md)"
severity: "medium"
- id: "halt_before_menu"
description: "Steps presenting user menus ([C] Continue, [a] Advanced, etc.) must HALT and wait for response"
severity: "high"
# ============================================
# XML WORKFLOW/TASK RULES
# ============================================
xml_workflows:
description: "XML-based workflows and tasks"
globs:
- "src/**/workflows/**/*.xml"
- "src/**/tasks/**/*.xml"
rules:
- id: "xml_task_id_required"
description: "XML tasks must have unique 'id' attribute on root task element"
severity: "high"
- id: "xml_llm_instructions"
description: "XML workflows should include <llm> section with critical execution instructions for the agent"
severity: "medium"
- id: "xml_step_numbering"
description: "XML steps should use n='X' attribute for sequential numbering"
severity: "medium"
- id: "xml_action_tags"
description: "Use <action> for required actions, <ask> for user input (must HALT), <goto> for jumps, <check if='...'> for conditionals"
severity: "medium"
- id: "xml_ask_must_halt"
description: "<ask> tags require agent to HALT and wait for user response before continuing"
severity: "high"
# ============================================
# WORKFLOW CONTENT QUALITY
# ============================================
workflow_content:
description: "Content quality and consistency rules for all workflow files"
globs:
- "src/**/workflows/**/*.md"
- "src/**/workflows/**/*.yaml"
rules:
- id: "communication_language_variable"
description: "Workflows should use {communication_language} variable for agent output language consistency"
severity: "low"
- id: "path_placeholders_required"
description: "Use path placeholders (e.g. {project-root}, {installed_path}, {output_folder}) instead of hardcoded paths"
severity: "medium"
- id: "no_time_estimates"
description: "Workflows should NOT include time estimates - AI development speed varies significantly"
severity: "low"
- id: "facilitator_not_generator"
description: "Workflow agents should act as facilitators (guide user input) not content generators (create without input)"
severity: "medium"
- id: "no_skip_optimization"
description: "Workflows must execute steps sequentially - no skipping or 'optimizing' step order"
severity: "high"
# ============================================
# AGENT DEFINITIONS
# ============================================
agent_definitions:
description: "Agent YAML configuration files"
globs:
- "src/**/*.agent.yaml"
rules:
- id: "agent_metadata_required"
description: "Agent files must have metadata section with id, name, title, icon, and module"
severity: "high"
- id: "agent_persona_required"
description: "Agent files must define persona with role, identity, communication_style, and principles"
severity: "high"
- id: "agent_menu_valid_workflows"
description: "Menu triggers must reference valid workflow paths that exist"
severity: "high"
# ============================================
# TEMPLATES
# ============================================
templates:
description: "Template files for workflow outputs"
globs:
- "src/**/template*.md"
- "src/**/templates/**/*.md"
rules:
- id: "placeholder_syntax"
description: "Use {variable_name} or {{variable_name}} syntax consistently for placeholders"
severity: "medium"
- id: "template_sections_marked"
description: "Template sections that need generation should be clearly marked (e.g., <!-- GENERATE: section_name -->)"
severity: "low"
# ============================================
# DOCUMENTATION
# ============================================
documentation:
description: "Documentation files"
globs:
- "docs/**/*.md"
- "README.md"
- "CONTRIBUTING.md"
rules:
- id: "valid_internal_links"
description: "Internal markdown links must point to existing files"
severity: "medium"
# ============================================
# BUILD TOOLS
# ============================================
build_tools:
description: "Build scripts and tooling"
globs:
- "tools/**"
rules:
- id: "script_error_handling"
description: "Scripts should handle errors gracefully with proper exit codes"
severity: "medium"

View File

@ -17,21 +17,66 @@ reviews:
base_branches:
- main
path_filters:
# --- Shared baseline: tool configs ---
- "!.coderabbit.yaml"
- "!.augment/**"
- "!eslint.config.mjs"
# --- Shared baseline: build output ---
- "!dist/**"
- "!build/**"
- "!coverage/**"
# --- Shared baseline: vendored/generated ---
- "!**/node_modules/**"
- "!**/*.min.js"
- "!**/*.generated.*"
- "!**/*.bundle.md"
# --- Shared baseline: package metadata ---
- "!package-lock.json"
# --- Shared baseline: binary/media ---
- "!*.png"
- "!*.jpg"
- "!*.svg"
# --- Shared baseline: test fixtures ---
- "!test/fixtures/**"
- "!test/template-test-generator/**"
- "!tools/template-test-generator/test-scenarios/**"
# --- Shared baseline: non-project dirs ---
- "!_bmad*/**"
- "!website/**"
- "!z*/**"
- "!sample-project/**"
- "!test-project-install/**"
# --- Shared baseline: AI assistant dirs ---
- "!.claude/**"
- "!.codex/**"
- "!.agent/**"
- "!.agentvibes/**"
- "!.kiro/**"
- "!.roo/**"
- "!.github/chatmodes/**"
# --- Shared baseline: build temp ---
- "!.bundler-temp/**"
# --- Shared baseline: generated reports ---
- "!**/validation-report-*.md"
- "!CHANGELOG.md"
path_instructions:
- path: "**/*"
instructions: |
Focus on inconsistencies, contradictions, edge cases and serious issues.
Avoid commenting on minor issues such as linting, formatting and style issues.
When providing code suggestions, use GitHub's suggestion format:
```suggestion
<code changes>
```
- path: "**/*.js"
instructions: |
CLI tooling code. Check for: missing error handling on fs operations,
path.join vs string concatenation, proper cleanup in error paths.
Flag any process.exit() without error message.
You are a cynical, jaded reviewer with zero patience for sloppy work.
This PR was submitted by a clueless weasel and you expect to find problems.
Be skeptical of everything.
Look for what's missing, not just what's wrong.
Use a precise, professional tone — no profanity or personal attacks.
Review with extreme skepticism — assume problems exist.
Find at least 10 issues to fix or improve.
Do NOT:
- Comment on formatting, linting, or style
- Give "looks good" passes
- Anchor on any specific ruleset — reason freely
If you find zero issues, re-analyze — this is suspicious.
chat:
auto_reply: true # Response to mentions in comments, a la @coderabbit review
issue_enrichment:

View File

@ -1,7 +1,7 @@
name: Trigger CodeRabbit on Ready for Review
on:
pull_request:
pull_request_target:
types: [ready_for_review]
jobs:

View File

@ -6,7 +6,6 @@ on:
- main
paths:
- "docs/**"
- "src/modules/*/docs/**"
- "website/**"
- "tools/build-docs.mjs"
- ".github/workflows/docs.yaml"
@ -19,6 +18,7 @@ permissions:
concurrency:
group: "pages"
# No big win in setting this to true — risk of cancelling a deploy mid-flight.
cancel-in-progress: false
jobs:
@ -28,12 +28,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
with:
# Full history needed for Starlight's lastUpdated timestamps (git log)
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: ".nvmrc"
cache: "npm"
- name: Install dependencies

View File

@ -84,10 +84,8 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Validate documentation links
run: npm run docs:validate-links
- name: Build documentation
# Note: build-docs.mjs runs link validation internally before building
run: npm run docs:build
validate:

4
.gitignore vendored
View File

@ -42,7 +42,9 @@ z*/
_bmad
_bmad-output
.clinerules
.augment
# .augment/ is gitignored except tracked config files — add exceptions explicitly
.augment/*
!.augment/code_review_guidelines.yaml
.crush
.cursor
.iflow

View File

@ -1,5 +1,58 @@
# Changelog
## [6.0.0-Beta.8]
**Release: February 8, 2026**
### 🌟 Key Highlights
1. **Non-Interactive Installation** — Full CI/CD support with 10 new CLI flags for automated deployments
2. **Complete @clack/prompts Migration** — Unified CLI experience with consolidated installer output
3. **CSV File Reference Validation** — Extended Layer 1 validator to catch broken workflow references in CSV files
4. **Kiro IDE Support** — Standardized config-driven installation, replacing custom installer
### 🎁 Features
* **Non-Interactive Installation** — Added `--directory`, `--modules`, `--tools`, `--custom-content`, `--user-name`, `--communication-language`, `--document-output-language`, `--output-folder`, and `-y/--yes` flags for CI/CD automation (#1520)
* **CSV File Reference Validation** — Extended validator to scan `.csv` files for broken workflow references, checking 501 references across 212 files (#1573)
* **Kiro IDE Support** — Replaced broken custom installer with config-driven templates using `#[[file:...]]` syntax and `inclusion: manual` frontmatter (#1589)
* **OpenCode Template Consolidation** — Combined split templates with `mode: primary` frontmatter for Tab-switching support, fixing agent discovery (#1556)
* **Modules Reference Page** — Added official external modules reference documentation (#1540)
### 🐛 Bug Fixes
* **Installer Streamlining** — Removed "None - Skip module installation" option, eliminated ~100 lines of dead code, and added ESM/.cjs support for module installers (#1590)
* **CodeRabbit Workflow** — Changed `pull_request` to `pull_request_target` to fix 403 errors and enable reviews on fork PRs (#1583)
* **Party Mode Return Protocol** — Added RETURN PROTOCOL to prevent lost-in-the-middle failures after Party Mode completes (#1569)
* **Spacebar Toggle** — Fixed SPACE key not working in autocomplete multiselect prompts for tool/IDE selection (#1557)
* **OpenCode Agent Routing** — Fixed agents installing to wrong directory by adding `targets` array for routing `.opencode/agent/` vs `.opencode/command/` (#1549)
* **Technical Research Workflow** — Fixed step-05 routing to step-06 and corrected `stepsCompleted` values (#1547)
* **Forbidden Variable Removal** — Removed `workflow_path` variable from 16 workflow step files (#1546)
* **Kilo Installer** — Fixed YAML formatting issues by trimming activation header and converting to yaml.parse/stringify (#1537)
* **bmad-help** — Now reads project-specific docs and respects `communication_language` setting (#1535)
* **Cache Errors** — Removed `--prefer-offline` npm flag to prevent stale cache errors during installation (#1531)
### ♻️ Refactoring
* **Complete @clack/prompts Migration** — Migrated 24 files from legacy libraries (ora, chalk, boxen, figlet, etc.), replaced ~100 console.log+chalk calls, consolidated installer output to single spinner, and removed 5 dependencies (#1586)
* **Downloads Page Removal** — Removed downloads page, bundle generation, and archiver dependency in favor of GitHub's native archives (#1577)
* **Workflow Verb Standardization** — Replaced "invoke/run" with "load and follow/load" in review workflow prompts (#1570)
* **Documentation Language** — Renamed "brownfield" to "established projects" and flattened directory structure for accessibility (#1539)
### 📚 Documentation
* **Comprehensive Site Review** — Fixed broken directory tree diagram, corrected grammar/capitalization, added SEO descriptions, and reordered how-to guides (#1578)
* **SEO Metadata** — Added description front matter to 9 documentation pages for search engine optimization (#1566)
* **PR Template** — Added pull request template for consistent PR descriptions (#1554)
* **Manual Release Cleanup** — Removed broken manual-release workflow and related scripts (#1576)
### 🔧 Maintenance
* **Dual-Mode AI Code Review** — Configured Augment Code (audit mode) and CodeRabbit (adversarial mode) for improved code quality (#1511)
* **Package-Lock Sync** — Cleaned up 471 lines of orphaned dependencies after archiver removal (#1580)
---
## [6.0.0-Beta.7]
**Release: February 4, 2026**

View File

@ -147,6 +147,15 @@ Keep messages under 72 characters. Each commit = one logical change.
- Everything is natural language (markdown) — no code in core framework
- Use BMad modules for domain-specific features
- Validate YAML schemas: `npm run validate:schemas`
- Validate file references: `npm run validate:refs`
### File-Pattern-to-Validator Mapping
| File Pattern | Validator | Extraction Function |
| ------------ | --------- | ------------------- |
| `*.yaml`, `*.yml` | `validate-file-refs.js` | `extractYamlRefs` |
| `*.md`, `*.xml` | `validate-file-refs.js` | `extractMarkdownRefs` |
| `*.csv` | `validate-file-refs.js` | `extractCsvRefs` |
---

View File

@ -36,7 +36,7 @@ Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, Windsu
npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes
```
See [Non-Interactive Installation Guide](docs/non-interactive-installation.md) for all available options.
See [Non-Interactive Installation Guide](http://docs.bmad-method.org/how-to/non-interactive-installation/) for all available options.
> **Not sure what to do?** Run `/bmad-help` — it tells you exactly what's next and what's optional. You can also ask it questions like:

View File

@ -6,4 +6,4 @@ template: splash
The page you're looking for doesn't exist or has been moved.
[Return to Home](/docs/index.md)
[Return to Home](./index.md)

View File

@ -1,5 +1,6 @@
---
title: "Documentation Style Guide"
description: Project-specific documentation conventions based on Google style and Diataxis structure
---
This project adheres to the [Google Developer Documentation Style Guide](https://developers.google.com/style) and uses [Diataxis](https://diataxis.fr/) to structure content. Only project-specific conventions follow.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

View File

@ -1,375 +0,0 @@
---
title: "Game Types Reference"
draft: true
---
BMGD supports 24 game type templates. Each adds genre-specific sections to your GDD.
## Game Types
### Action & Combat
#### Action Platformer
Side-scrolling or 3D platforming with combat mechanics.
**Examples:** Hollow Knight, Mega Man, Celeste
**GDD sections:**
- Movement systems (jumps, dashes, wall mechanics)
- Combat mechanics (melee/ranged, combos)
- Level design patterns
- Boss design
#### Shooter
Projectile combat with aiming mechanics.
**Examples:** Doom, Call of Duty, Splatoon
**GDD sections:**
- Weapon systems
- Aiming and accuracy
- Enemy AI patterns
- Level/arena design
- Multiplayer considerations
#### Fighting
1v1 combat with combos and frame data.
**Examples:** Street Fighter, Tekken, Super Smash Bros.
**GDD sections:**
- Frame data systems
- Combo mechanics
- Character movesets
- Competitive balance
- Netcode requirements
### Strategy & Tactics
#### Strategy
Resource management with tactical decisions.
**Examples:** StarCraft, Civilization, Europa Universalis
**GDD sections:**
- Resource systems
- Unit/building design
- AI opponent behavior
- Map/scenario design
- Victory conditions
#### Turn-Based Tactics
Grid-based movement with turn order.
**Examples:** XCOM, Fire Emblem, Into the Breach
**GDD sections:**
- Grid and movement systems
- Turn order mechanics
- Cover and positioning
- Unit progression
- Procedural mission generation
#### Tower Defense
Wave-based defense with tower placement.
**Examples:** Bloons TD, Kingdom Rush, Plants vs. Zombies
**GDD sections:**
- Tower types and upgrades
- Wave design and pacing
- Economy systems
- Map design patterns
- Meta-progression
### RPG & Progression
#### RPG
Character progression with stats, inventory, and quests.
**Examples:** Final Fantasy, The Witcher, Baldur's Gate
**GDD sections:**
- Character stats and leveling
- Inventory and equipment
- Quest system design
- Combat system (action/turn-based)
- Skill trees and builds
#### Roguelike
Procedural generation with permadeath and run-based progression.
**Examples:** Hades, Dead Cells, Spelunky
**GDD sections:**
- Procedural generation rules
- Permadeath and persistence
- Run structure and pacing
- Item/ability synergies
- Meta-progression systems
#### Metroidvania
Interconnected world with ability gating.
**Examples:** Metroid, Castlevania: Symphony of the Night, Ori
**GDD sections:**
- World map connectivity
- Ability gating design
- Backtracking flow
- Secret and collectible placement
- Power-up progression
### Narrative & Story
#### Adventure
Story-driven exploration with puzzle elements.
**Examples:** Monkey Island, Myst, Life is Strange
**GDD sections:**
- Puzzle design
- Narrative delivery
- Exploration mechanics
- Dialogue systems
- Story branching
#### Visual Novel
Narrative choices with branching story.
**Examples:** Doki Doki Literature Club, Phoenix Wright, Steins;Gate
**GDD sections:**
- Branching narrative structure
- Choice and consequence
- Character routes
- UI/presentation
- Save/load states
#### Text-Based
Text input/output games with parser or choice mechanics.
**Examples:** Zork, 80 Days, Dwarf Fortress (adventure mode)
**GDD sections:**
- Parser or choice systems
- World model
- Narrative structure
- Text presentation
- Save state management
### Simulation & Management
#### Simulation
Realistic systems with management and building.
**Examples:** SimCity, RollerCoaster Tycoon, The Sims
**GDD sections:**
- Core simulation loops
- Economy modeling
- AI agents/citizens
- Building/construction
- Failure states
#### Sandbox
Creative freedom with building and minimal objectives.
**Examples:** Minecraft, Terraria, Garry's Mod
**GDD sections:**
- Creation tools
- Physics/interaction systems
- Persistence and saving
- Sharing/community features
- Optional objectives
### Sports & Racing
#### Racing
Vehicle control with tracks and lap times.
**Examples:** Mario Kart, Forza, Need for Speed
**GDD sections:**
- Vehicle physics model
- Track design
- AI opponents
- Progression/career mode
- Multiplayer racing
#### Sports
Team-based or individual sports simulation.
**Examples:** FIFA, NBA 2K, Tony Hawk's Pro Skater
**GDD sections:**
- Sport-specific rules
- Player/team management
- AI opponent behavior
- Season/career modes
- Multiplayer modes
### Multiplayer
#### MOBA
Multiplayer team battles with hero selection.
**Examples:** League of Legends, Dota 2, Smite
**GDD sections:**
- Hero/champion design
- Lane and map design
- Team composition
- Matchmaking
- Economy (gold/items)
#### Party Game
Local multiplayer with minigames.
**Examples:** Mario Party, Jackbox, Overcooked
**GDD sections:**
- Minigame design patterns
- Controller support
- Round/game structure
- Scoring systems
- Player count flexibility
### Horror & Survival
#### Survival
Resource gathering with crafting and persistent threats.
**Examples:** Don't Starve, Subnautica, The Forest
**GDD sections:**
- Resource gathering
- Crafting systems
- Hunger/health/needs
- Threat systems
- Base building
#### Horror
Atmosphere and tension with limited resources.
**Examples:** Resident Evil, Silent Hill, Amnesia
**GDD sections:**
- Fear mechanics
- Resource scarcity
- Sound design
- Lighting and visibility
- Enemy/threat design
### Casual & Progression
#### Puzzle
Logic-based challenges and problem-solving.
**Examples:** Tetris, Portal, The Witness
**GDD sections:**
- Puzzle mechanics
- Difficulty progression
- Hint systems
- Level structure
- Scoring/rating
#### Idle/Incremental
Passive progression with upgrades and automation.
**Examples:** Cookie Clicker, Adventure Capitalist, Clicker Heroes
**GDD sections:**
- Core loop design
- Prestige systems
- Automation unlocks
- Number scaling
- Offline progress
#### Card Game
Deck building with card mechanics.
**Examples:** Slay the Spire, Hearthstone, Magic: The Gathering Arena
**GDD sections:**
- Card design framework
- Deck building rules
- Mana/resource systems
- Rarity and collection
- Competitive balance
### Rhythm
#### Rhythm
Music synchronization with timing-based gameplay.
**Examples:** Guitar Hero, Beat Saber, Crypt of the NecroDancer
**GDD sections:**
- Note/beat mapping
- Scoring systems
- Difficulty levels
- Music licensing
- Input methods
## Hybrid Types
Multiple game types can be combined. GDD sections from all selected types are included.
| Hybrid | Components | Combined Sections |
|--------|------------|-------------------|
| Action RPG | Action Platformer + RPG | Movement, combat, stats, inventory |
| Survival Horror | Survival + Horror | Resources, crafting, atmosphere, fear |
| Roguelike Deckbuilder | Roguelike + Card Game | Run structure, procedural gen, cards |
| Tactical RPG | Turn-Based Tactics + RPG | Grid movement, stats, progression |
| Open World Survival | Sandbox + Survival | Building, crafting, exploration |

View File

@ -1,113 +0,0 @@
---
title: "BMGD Quick Guide"
description: Quick reference for BMad Game Dev Studio
draft: true
---
![BMGD Logo](bmgd-logo.png)
# BMGD Quick Guide
BMad Game Dev Studio (BMGD) extends BMM with game-specific capabilities. Developed by game industry veterans, it guides you through product research, technical design, narrative design, and a full epic-driven production cycle.
## Under Construction
Documentation is under heavy construction catching up with the new beta release. We'll have complete documentation up as soon as possible. For now, please ask in the BMGD section of the Discord if you have any questions.
![BMGD Workflow](workflow.jpg)
## Quick Start
**Install → Game Brief → GDD → (Narrative) → Architecture → Build**
BMGD is an optional module installed via BMAD Method: `npx bmad-method install`
See [How-To Reference](#how-to-reference) for commands.
## Development Phases
| Phase | Name | Key Activities |
|-------|------|----------------|
| 1 | **Preproduction** | Brainstorm Game, Game Brief, market research |
| 2 | **Design** | GDD creation, Narrative Design (for story-driven games) |
| 3 | **Technical** | Game Architecture (engine, systems, patterns) |
| 4 | **Production** | Sprint planning, story development, code review, testing |
## BMGD Agents
| Agent | Purpose |
|-------|---------|
| Game Designer | Game mechanics, balance, player psychology |
| Game Developer | Implementation with engine-specific patterns |
| Game Architect | Engine selection, systems design, technical structure |
| Game Scrum Master | Sprint planning and epic management |
| Game QA | Playtesting, engine-specific testing, performance profiling |
| Game Solo Dev | Full-stack game development for solo projects |
## Key Documents
| Document | Purpose |
|----------|---------|
| **Game Brief** | Vision, market positioning, fundamentals |
| **GDD** | Core loop, mechanics, progression, art/audio direction |
| **Narrative Design** | Story structure, characters, world-building, dialogue |
| **Architecture** | Engine, systems, patterns, project structure |
## Game Type Templates
BMGD includes 24 game type templates that auto-configure GDD sections:
Action, Adventure, Puzzle, RPG, Strategy, Simulation, Sports, Racing, Fighting, Horror, Platformer, Shooter, and more.
Each template provides genre-specific GDD sections, mechanics patterns, testing considerations, and common pitfalls to avoid.
## Explanation: BMGD vs BMM
### When to Use Each
| Use BMGD for | Use BMM for |
|--------------|-------------|
| Video games | Web applications |
| Interactive experiences | APIs and services |
| Game prototyping | Mobile apps (non-game) |
| Game jams | General software projects |
### Phase Mapping
| BMM Phase | BMGD Phase | Key Difference |
|-----------|------------|----------------|
| Analysis | Preproduction | Game concepts, Game Brief instead of Product Brief |
| Planning | Design | GDD instead of PRD; optional Narrative Design |
| Solutioning | Technical | Focus on engine selection, game-specific patterns |
| Implementation | Production | Game QA replaces TEA; engine-specific testing |
### Document Differences
| BMM | BMGD | Notes |
|-----|------|-------|
| Product Brief | Game Brief | Captures vision, market, fundamentals |
| PRD | GDD | Includes mechanics, balance, player experience |
| N/A | Narrative Design | Story, characters, world (story-driven games) |
| Architecture | Architecture | BMGD version includes engine-specific patterns and considerations |
### Testing Differences
**BMM (TEA):** Web-focused testing with Playwright, Cypress, API testing, E2E for web apps.
**BMGD (Game QA):** Engine-specific frameworks (Unity, Unreal, Godot), gameplay testing, performance profiling, playtest planning, balance validation.
## How-To Reference
| I need to... | Action |
|--------------|--------------------------------------------------------------------------------------------------------|
| Install BMGD | Run `npx bmad-method install` and select BMGD during module installation |
| Start a new game | Run `/bmad-gds-brainstorm-game`, then `/bmad:gds:create-game-brief` |
| Design my game | Run `/bmad-gds-create-gdd`; add `/bmad:gds:narrative` if story-heavy |
| Plan architecture | Run `/bmad-gds-game-architecture` with Game Architect |
| Build my game | Use Phase 4 production workflows - Run `/bmad-help` to see what's next |
| Test an idea quickly | Use [Quick-Flow](quick-flow-workflows.md) for rapid prototyping |
## Further Reading
- [Game Types Guide](game-types.md)
- [Quick-Flow Guide](quick-flow-workflows.md)

View File

@ -1,161 +0,0 @@
---
title: "Quick Flow Workflows"
draft: true
---
How to create tech specs and execute implementations with Quick Flow.
## Choosing a Workflow
| Situation | Workflow | Command |
|-----------|----------|---------|
| Need to document before implementing | Quick-Spec | `/bmad-gds-quick-spec` |
| Multiple approaches to evaluate | Quick-Spec | `/bmad-gds-quick-spec` |
| Have a completed tech-spec | Quick-Dev | `/bmad-gds-quick-dev path/to/spec.md` |
| Have clear, direct instructions | Quick-Dev | `/bmad-gds-quick-dev` |
| Building complete game system | Full GDS | `/bmad-gds-workflow-init` |
| Epic-level features | Full GDS | `/bmad-gds-workflow-init` |
---
## How to Create a Tech Spec (Quick-Spec)
### Step 1: Start the workflow
```bash
/bmad-gds-quick-spec
```
### Step 2: Describe your requirement
Provide your feature request. The agent scans the codebase and asks clarifying questions.
**Checkpoint options:**
- `[a]` Advanced Elicitation - explore requirements deeper
- `[c]` Continue to investigation
- `[p]` Party Mode - consult expert agents
### Step 3: Review investigation findings
The agent analyzes the codebase for patterns, constraints, and similar implementations. Review the findings.
**Checkpoint options:**
- `[c]` Continue to spec generation
- `[p]` Party Mode - get technical review
### Step 4: Review generated spec
The agent creates an ordered task list with file paths and acceptance criteria. Verify completeness.
**Checkpoint options:**
- `[c]` Continue to final review
- `[p]` Party Mode - technical review
### Step 5: Finalize
Confirm the spec meets these standards:
- Every task has a file path and specific action
- Tasks ordered by dependency
- Acceptance criteria in Given/When/Then format
- No placeholders or TBD sections
**Options:**
- `[d]` Start Quick-Dev immediately
- `[done]` Save spec and exit
**Output:** `{planning_artifacts}/tech-spec-{slug}.md`
---
## How to Execute Implementation (Quick-Dev)
### With a Tech-Spec
```bash
/bmad-gds-quick-dev path/to/tech-spec-feature.md
```
The agent:
1. Captures baseline git commit
2. Loads and validates the spec
3. Executes tasks in order
4. Runs self-check
5. Performs adversarial review
6. Resolves findings
7. Validates against acceptance criteria
### With Direct Instructions
```bash
/bmad-gds-quick-dev
```
Then describe what you want implemented:
1. Captures baseline git commit
2. Evaluates complexity (may suggest planning)
3. Gathers context from codebase
4. Executes implementation
5. Runs self-check and adversarial review
6. Resolves findings
**Escalation:** If the agent detects complexity (multiple components, system-level scope, uncertainty), it offers:
- `[t]` Create tech-spec first
- `[w]` Use full GDS workflow
- `[e]` Execute anyway
---
## Troubleshooting
### Spec has placeholders or TBD sections
Return to investigation step. Complete missing research, inline all findings, re-run review.
### Workflow lost context mid-step
Check frontmatter for `stepsCompleted`. Resume from last completed step.
### Agent suggested planning but you want to execute
You can override with `[e]`, but document your assumptions. Escalation heuristics exist because planning saves time on complex tasks.
### Tests failing after implementation
Return to the resolve-findings step. Review failures, fix issues, ensure test expectations are correct, re-run full suite.
### Need help
```bash
/bmad-help
```
---
## Reference
### File Locations
| File | Location |
|------|----------|
| Work in progress | `{implementation_artifacts}/tech-spec-wip.md` |
| Completed specs | `{planning_artifacts}/tech-spec-{slug}.md` |
| Archived specs | `{implementation_artifacts}/tech-spec-{slug}-archived-{date}.md` |
| Workflow files | `_bmad/gds/workflows/gds-quick-flow/` |
### Validation Criteria
**Self-check (before adversarial review):**
- All tasks/instructions completed
- Tests written and passing
- Follows existing patterns
- No obvious bugs
- Acceptance criteria met
- Code is readable
**Adversarial review:**
- Correctness
- Security
- Performance
- Maintainability
- Test coverage
- Error handling

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View File

@ -1,11 +1,17 @@
---
title: "Advanced Elicitation"
description: Push the LLM to rethink its work using structured reasoning methods
sidebar:
order: 6
---
Make the LLM reconsider what it just generated. You pick a reasoning method, it applies that method to its own output, you decide whether to keep the improvements.
Dozens of methods are built in - things like First Principles, Red Team vs Blue Team, Pre-mortem Analysis, Socratic Questioning, and more.
## What is Advanced Elicitation?
A structured second pass. Instead of asking the AI to "try again" or "make it better," you select a specific reasoning method and the AI re-examines its own output through that lens.
The difference matters. Vague requests produce vague revisions. A named method forces a particular angle of attack, surfacing insights that a generic retry would miss.
## When to Use It
@ -22,3 +28,22 @@ Workflows offer advanced elicitation at decision points - after the LLM has gene
2. You pick one (or reshuffle for different options)
3. Method is applied, improvements shown
4. Accept or discard, repeat or continue
## Built-in Methods
Dozens of reasoning methods are available. A few examples:
- **Pre-mortem Analysis** - Assume the project already failed, work backward to find why
- **First Principles Thinking** - Strip away assumptions, rebuild from ground truth
- **Inversion** - Ask how to guarantee failure, then avoid those things
- **Red Team vs Blue Team** - Attack your own work, then defend it
- **Socratic Questioning** - Challenge every claim with "why?" and "how do you know?"
- **Constraint Removal** - Drop all constraints, see what changes, add them back selectively
- **Stakeholder Mapping** - Re-evaluate from each stakeholder's perspective
- **Analogical Reasoning** - Find parallels in other domains and apply their lessons
And many more. The AI picks the most relevant options for your content - you choose which to run.
:::tip[Start Here]
Pre-mortem Analysis is a good first pick for any spec or plan. It consistently finds gaps that a standard review misses.
:::

View File

@ -1,6 +1,8 @@
---
title: "Adversarial Review"
description: Forced reasoning technique that prevents lazy "looks good" reviews
sidebar:
order: 5
---
Force deeper analysis by requiring problems to be found.
@ -24,7 +26,7 @@ Normal reviews suffer from confirmation bias. You skim the work, nothing jumps o
## Where It's Used
Adversarial review appears throughout BMAD workflows - code review, implementation readiness checks, spec validation, and others. Sometimes it's a required step, sometimes optional (like advanced elicitation or party mode). The pattern adapts to whatever artifact needs scrutiny.
Adversarial review appears throughout BMad workflows - code review, implementation readiness checks, spec validation, and others. Sometimes it's a required step, sometimes optional (like advanced elicitation or party mode). The pattern adapts to whatever artifact needs scrutiny.
## Human Filtering Required

View File

@ -1,6 +1,8 @@
---
title: "Brainstorming"
description: Interactive creative sessions using 60+ proven ideation techniques
sidebar:
order: 2
---
Unlock your creativity through guided exploration.

View File

@ -1,6 +1,8 @@
---
title: "Established Projects FAQ"
description: Common questions about using BMad Method on established projects
sidebar:
order: 8
---
Quick answers to common questions about working on established projects with the BMad Method (BMM).

View File

@ -1,6 +1,8 @@
---
title: "Party Mode"
description: Multi-agent collaboration - get all your AI agents in one conversation
sidebar:
order: 7
---
Get all your AI agents in one conversation.

View File

@ -1,6 +1,8 @@
---
title: "Preventing Agent Conflicts"
description: How architecture prevents conflicts when multiple agents implement a system
sidebar:
order: 4
---
When multiple AI agents implement different parts of a system, they can make conflicting technical decisions. Architecture documentation prevents this by establishing shared standards.
@ -69,7 +71,7 @@ Explicit documentation of:
Think of architecture as the shared context that all agents read before implementing:
```
```text
PRD: "What to build"
Architecture: "How to build it"

View File

@ -1,27 +1,73 @@
---
title: "Quick Flow"
description: Fast-track for small changes - skip the full methodology
sidebar:
order: 1
---
Quick Flow is for when you don't need the full BMad Method. Skip Product Brief, PRD, and Architecture - go straight to implementation.
## How It Works
1. **Run `quick-spec`** — generates a focused tech-spec
2. **Run `quick-dev`** — implements it
That's it.
Skip the ceremony. Quick Flow takes you from idea to working code in two commands - no Product Brief, no PRD, no Architecture doc.
## When to Use It
- Bug fixes
- Refactoring
- Small features
- Prototyping
- Bug fixes and patches
- Refactoring existing code
- Small, well-understood features
- Prototyping and spikes
- Single-agent work where one developer can hold the full scope
## When to Use Full BMad Method Instead
## When NOT to Use It
- New products
- Major features
- Multiple teams involved
- Stakeholder alignment needed
- New products or platforms that need stakeholder alignment
- Major features spanning multiple components or teams
- Work that requires architectural decisions (database schema, API contracts, service boundaries)
- Anything where requirements are unclear or contested
:::caution[Scope Creep]
If you start a Quick Flow and realize the scope is bigger than expected, `quick-dev` will detect this and offer to escalate. You can switch to a full PRD workflow at any point without losing your work.
:::
## How It Works
Quick Flow has two commands, each backed by a structured workflow. You can run them together or independently.
### quick-spec: Plan
Run `quick-spec` and Barry (the Quick Flow agent) walks you through a conversational discovery process:
1. **Understand** - You describe what you want to build. Barry scans the codebase to ask informed questions, then captures a problem statement, solution approach, and scope boundaries.
2. **Investigate** - Barry reads relevant files, maps code patterns, identifies files to modify, and documents the technical context.
3. **Generate** - Produces a complete tech-spec with ordered implementation tasks (specific file paths and actions), acceptance criteria in Given/When/Then format, testing strategy, and dependencies.
4. **Review** - Presents the full spec for your sign-off. You can edit, ask questions, run adversarial review, or refine with advanced elicitation before finalizing.
The output is a `tech-spec-{slug}.md` file saved to your project's implementation artifacts folder. It contains everything a fresh agent needs to implement the feature - no conversation history required.
### quick-dev: Build
Run `quick-dev` and Barry implements the work. It operates in two modes:
- **Tech-spec mode** - Point it at a spec file (`quick-dev tech-spec-auth.md`) and it executes every task in order, writes tests, and verifies acceptance criteria.
- **Direct mode** - Give it instructions directly (`quick-dev "refactor the auth middleware"`) and it gathers context, builds a mental plan, and executes.
After implementation, `quick-dev` runs a self-check audit against all tasks and acceptance criteria, then triggers an adversarial code review of the diff. Findings are presented for you to resolve before wrapping up.
:::tip[Fresh Context]
For best results, run `quick-dev` in a new conversation after finishing `quick-spec`. This gives the implementation agent clean context focused solely on building.
:::
## What Quick Flow Skips
The full BMad Method produces a Product Brief, PRD, Architecture doc, and Epic/Story breakdown before any code is written. Quick Flow replaces all of that with a single tech-spec. This works because Quick Flow targets changes where:
- The product direction is already established
- Architecture decisions are already made
- A single developer can reason about the full scope
- Requirements fit in one conversation
## Escalating to Full BMad Method
Quick Flow includes built-in guardrails for scope detection. When you run `quick-dev` with a direct request, it evaluates signals like multi-component mentions, system-level language, and uncertainty about approach. If it detects the work is bigger than a quick flow:
- **Light escalation** - Recommends running `quick-spec` first to create a plan
- **Heavy escalation** - Recommends switching to the full BMad Method PRD process
You can also escalate manually at any time. Your tech-spec work carries forward - it becomes input for the broader planning process rather than being discarded.

View File

@ -1,6 +1,8 @@
---
title: "Why Solutioning Matters"
description: Understanding why the solutioning phase is critical for multi-epic projects
sidebar:
order: 3
---
@ -8,7 +10,7 @@ Phase 3 (Solutioning) translates **what** to build (from Planning) into **how**
## The Problem Without Solutioning
```
```text
Agent 1 implements Epic 1 using REST API
Agent 2 implements Epic 2 using GraphQL
Result: Inconsistent API design, integration nightmare
@ -18,7 +20,7 @@ When multiple agents implement different parts of a system without shared archit
## The Solution With Solutioning
```
```text
architecture workflow decides: "Use GraphQL for all APIs"
All agents follow architecture decisions
Result: Consistent implementation, no conflicts

View File

@ -1,33 +1,35 @@
---
title: "BMad Method Customization Guide"
title: "How to Customize BMad"
description: Customize agents, workflows, and modules while preserving update compatibility
sidebar:
order: 7
---
The ability to customize the BMad Method and its core to your needs, while still being able to get updates and enhancements is a critical idea within the BMad Ecosystem.
Use the `.customize.yaml` files to tailor agent behavior, personas, and menus while preserving your changes across updates.
The Customization Guidance outlined here, while targeted at understanding BMad Method customization, applies to any other module use within the BMad Method.
## When to Use This
## Types of Customization
- You want to change an agent's name, personality, or communication style
- You need agents to remember project-specific context
- You want to add custom menu items that trigger your own workflows or prompts
- You want agents to perform specific actions every time they start up
Customization includes Agent Customization, Workflow/Skill customization, the addition of new MCPs or Skills to be used by existing agents. Aside from all of this, a whole other realm of customization involves creating / adding your own relevant BMad Builder workflows, skills, agents and maybe even your own net new modules to compliment the BMad Method Module.
:::note[Prerequisites]
- BMad installed in your project (see [How to Install BMad](./install-bmad.md))
- A text editor for YAML files
:::
Warning: The reason for customizing as this guide will prescribe will allow you to continue getting updates without worrying about losing your customization changes. And by continuing to get updates as BMad modules advance, you will be able to continue to evolve as the system improves.
:::caution[Keep Your Customizations Safe]
Always use the `.customize.yaml` files described here rather than editing agent files directly. The installer overwrites agent files during updates, but preserves your `.customize.yaml` changes.
:::
## Agent Customization
## Steps
### Agent Customization Areas
### 1. Locate Customization Files
- Change agent names, personas or manner of speech
- Add project-specific memories or context
- Add custom menu items to custom or inline prompts, skills or custom BMad workflows
- Define critical actions that occur agent startup for consistent behavior
After installation, find one `.customize.yaml` file per agent in:
## How to customize an agent.
**1. Locate Customization Files**
After installation, find agent customization files in:
```
```text
_bmad/_config/agents/
├── core-bmad-master.customize.yaml
├── bmm-dev.customize.yaml
@ -35,28 +37,22 @@ _bmad/_config/agents/
└── ... (one file per installed agent)
```
**2. Edit Any Agent**
### 2. Edit the Customization File
Open the `.customize.yaml` file for the agent you want to modify. All sections are optional - customize only what you need.
Open the `.customize.yaml` file for the agent you want to modify. Every section is optional -- customize only what you need.
**3. Rebuild the Agent**
| Section | Behavior | Purpose |
| ------------------- | ------------ | ---------------------------------------------- |
| `agent.metadata` | Replaces | Override the agent's display name |
| `persona` | Replaces | Set role, identity, style, and principles |
| `memories` | Appends | Add persistent context the agent always recalls |
| `menu` | Appends | Add custom menu items for workflows or prompts |
| `critical_actions` | Appends | Define startup instructions for the agent |
| `prompts` | Appends | Create reusable prompts for menu actions |
After editing, IT IS CRITICAL to rebuild the agent to apply changes:
Sections marked **Replaces** overwrite the agent's defaults entirely. Sections marked **Appends** add to the existing configuration.
```bash
npx bmad-method install
```
You can either then:
- Select `Quick Update` - This will also ensure all packages are up to date AND compile all agents to include any updates or customizations
- Select `Rebuild Agents` - This will only rebuild and apply customizations to agents, without pulling the latest
There will be additional tools shortly after beta launch to allow install of individual agents, workflows, skills and modules without the need for using the full bmad installer.
### What Agent Properties Can Be Customized?
#### Agent Name
**Agent Name**
Change how the agent introduces itself:
@ -66,7 +62,7 @@ agent:
name: 'Spongebob' # Default: "Amelia"
```
#### Persona
**Persona**
Replace the agent's personality, role, and communication style:
@ -80,9 +76,9 @@ persona:
- 'Favor composition over inheritance'
```
**Note:** The persona section replaces the entire default persona (not merged).
The `persona` section replaces the entire default persona, so include all four fields if you set it.
#### Memories
**Memories**
Add persistent context the agent will always remember:
@ -90,12 +86,12 @@ Add persistent context the agent will always remember:
memories:
- 'Works at Krusty Krab'
- 'Favorite Celebrity: David Hasslehoff'
- 'Learned in Epic 1 that its not cool to just pretend that tests have passed'
- 'Learned in Epic 1 that it is not cool to just pretend that tests have passed'
```
### Custom Menu Items
**Menu Items**
Any custom items you add here will be included in the agents display menu.
Add custom entries to the agent's display menu. Each item needs a `trigger`, a target (`workflow` path or `action` reference), and a `description`:
```yaml
menu:
@ -107,18 +103,18 @@ menu:
description: Deploy to production
```
### Critical Actions
**Critical Actions**
Add instructions that execute before the agent starts:
Define instructions that run when the agent starts up:
```yaml
critical_actions:
- 'Check the CI Pipelines with the XYZ Skill and alert user on wake if anything is urgently needing attention'
```
### Custom Prompts
**Custom Prompts**
Define reusable prompts for `action="#id"` menu handlers:
Create reusable prompts that menu items can reference with `action="#id"`:
```yaml
prompts:
@ -130,29 +126,47 @@ prompts:
3. Execute deployment script
```
### 3. Apply Your Changes
After editing, recompile the agent to apply changes:
```bash
npx bmad-method install
```
The installer detects the existing installation and offers these options:
| Option | What It Does |
| --------------------- | ------------------------------------------------------------------- |
| **Quick Update** | Updates all modules to the latest version and recompiles all agents |
| **Recompile Agents** | Applies customizations only, without updating module files |
| **Modify BMad Installation** | Full installation flow for adding or removing modules |
For customization-only changes, **Recompile Agents** is the fastest option.
## Troubleshooting
**Changes not appearing?**
- Make sure you ran `npx bmad-method build <agent-name>` after editing
- Check YAML syntax is valid (indentation matters!)
- Verify the agent name matches the file name pattern
- Run `npx bmad-method install` and select **Recompile Agents** to apply changes
- Check that your YAML syntax is valid (indentation matters)
- Verify you edited the correct `.customize.yaml` file for the agent
**Agent not loading?**
- Check for YAML syntax errors
- Ensure required fields aren't left empty if you uncommented them
- Try reverting to the template and rebuilding
- Check for YAML syntax errors using an online YAML validator
- Ensure you did not leave fields empty after uncommenting them
- Try reverting to the original template and rebuilding
**Need to reset?**
**Need to reset an agent?**
- Remove content from the `.customize.yaml` file (or delete the file)
- Run `npx bmad-method build <agent-name>` to regenerate defaults
- Clear or delete the agent's `.customize.yaml` file
- Run `npx bmad-method install` and select **Recompile Agents** to restore defaults
## Workflow Customization
Information about customizing existing BMad Method workflows and skills are coming soon.
Customization of existing BMad Method workflows and skills is coming soon.
## Module Customization
Information on how to build expansion modules that augment BMad, or make other existing module customizations are coming soon.
Guidance on building expansion modules and customizing existing modules is coming soon.

View File

@ -1,6 +1,8 @@
---
title: "Established Projects"
description: How to use BMad Method on existing codebases
sidebar:
order: 6
---
Use BMad Method effectively when working on existing projects and legacy codebases, sometimes also referred to as brownfield projects.
@ -44,8 +46,8 @@ You have two primary options depending on the scope of changes:
| Scope | Recommended Approach |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| **Small updates or additions** | Use `quick-flow-solo-dev` to create a tech-spec and implement the change. The full four-phase BMad method is likely overkill. |
| **Major changes or additions** | Start with the BMad method, applying as much or as little rigor as needed. |
| **Small updates or additions** | Use `quick-flow-solo-dev` to create a tech-spec and implement the change. The full four-phase BMad Method is likely overkill. |
| **Major changes or additions** | Start with the BMad Method, applying as much or as little rigor as needed. |
### During PRD Creation
@ -76,5 +78,5 @@ Pay close attention here to prevent reinventing the wheel or making decisions th
## More Information
- **[Quick Fixes](/docs/how-to/quick-fixes.md)** - Bug fixes and ad-hoc changes
- **[Established Projects FAQ](/docs/explanation/established-projects-faq.md)** - Common questions about working on established projects
- **[Quick Fixes](./quick-fixes.md)** - Bug fixes and ad-hoc changes
- **[Established Projects FAQ](../explanation/established-projects-faq.md)** - Common questions about working on established projects

View File

@ -1,6 +1,8 @@
---
title: "How to Get Answers About BMad"
description: Use an LLM to quickly answer your own BMad questions
sidebar:
order: 4
---
If you have successfully installed BMad and the BMad Method (+ other modules as needed) - the first step in getting answers is `/bmad-help`. This will answer upwards of 80% of all questions and is available to you in the IDE as you are working.
@ -38,10 +40,11 @@ The `_bmad` folder is created when you install BMad. If you don't have it yet, c
Fetch `llms-full.txt` into your session:
```
```text
https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt
```
### 3. Ask Your Question
:::note[Example]

View File

@ -1,11 +1,13 @@
---
title: "How to Install BMad"
description: Step-by-step guide to installing BMad in your project
sidebar:
order: 1
---
Use the `npx bmad-method install` command to set up BMad in your project with your choice of modules and AI tools.
If you want to use a non interactive installer and provide all install options on the command line, [this guide](/docs/non-interactive-installation.md).
If you want to use a non interactive installer and provide all install options on the command line, see [this guide](./non-interactive-installation.md).
## When to Use This
@ -27,6 +29,13 @@ If you want to use a non interactive installer and provide all install options o
npx bmad-method install
```
:::tip[Bleeding edge]
To install the latest from the main branch (may be unstable):
```bash
npx github:bmad-code-org/BMAD-METHOD install
```
:::
### 2. Choose Installation Location
The installer will ask where to install BMad files:
@ -41,6 +50,7 @@ Pick which AI tools you use:
- Claude Code
- Cursor
- Windsurf
- Kiro
- Others
Each tool has its own way of integrating commands. The installer creates tiny prompt files to activate workflows and agents — it just puts them where your tool expects to find them.
@ -55,7 +65,7 @@ The installer guides you through the rest — custom content, settings, etc.
## What You Get
```
```text
your-project/
├── _bmad/
│ ├── bmm/ # Your selected modules
@ -63,22 +73,16 @@ your-project/
│ ├── core/ # Required core module
│ └── ...
├── _bmad-output/ # Generated artifacts
└── .claude/ # Claude Code commands (if using Claude Code)
├── .claude/ # Claude Code commands (if using Claude Code)
└── .kiro/ # Kiro steering files (if using Kiro)
```
## Verify Installation
Run the `help` workflow (`/bmad-help` on most platforms) to verify everything works and see what to do next.
**Latest from main branch:**
```bash
npx github:bmad-code-org/BMAD-METHOD install
```
Use these if you want the newest features before they're officially released. Things might break.
## Troubleshooting
**Installer throws an error** — Copy-paste the output into your AI assistant and let it figure it out.
**Installer worked but something doesn't work later** — Your AI needs BMad context to help. See [How to Get Answers About BMad](/docs/how-to/get-answers-about-bmad.md) for how to point your AI at the right sources.
**Installer worked but something doesn't work later** — Your AI needs BMad context to help. See [How to Get Answers About BMad](./get-answers-about-bmad.md) for how to point your AI at the right sources.

View File

@ -0,0 +1,171 @@
---
title: Non-Interactive Installation
description: Install BMad using command-line flags for CI/CD pipelines and automated deployments
sidebar:
order: 2
---
Use command-line flags to install BMad non-interactively. This is useful for:
## When to Use This
- Automated deployments and CI/CD pipelines
- Scripted installations
- Batch installations across multiple projects
- Quick installations with known configurations
:::note[Prerequisites]
Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm).
:::
## Available Flags
### Installation Options
| Flag | Description | Example |
|------|-------------|---------|
| `--directory <path>` | Installation directory | `--directory ~/projects/myapp` |
| `--modules <modules>` | Comma-separated module IDs | `--modules bmm,bmb` |
| `--tools <tools>` | Comma-separated tool/IDE IDs (use `none` to skip) | `--tools claude-code,cursor` or `--tools none` |
| `--custom-content <paths>` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | Action for existing installations: `install` (default), `update`, `quick-update`, or `compile-agents` | `--action quick-update` |
### Core Configuration
| Flag | Description | Default |
|------|-------------|---------|
| `--user-name <name>` | Name for agents to use | System username |
| `--communication-language <lang>` | Agent communication language | English |
| `--document-output-language <lang>` | Document output language | English |
| `--output-folder <path>` | Output folder path | _bmad-output |
### Other Options
| Flag | Description |
|------|-------------|
| `-y, --yes` | Accept all defaults and skip prompts |
| `-d, --debug` | Enable debug output for manifest generation |
## Module IDs
Available module IDs for the `--modules` flag:
- `bmm` — BMad Method Master
- `bmb` — BMad Builder
Check the [BMad registry](https://github.com/bmad-code-org) for available external modules.
## Tool/IDE IDs
Available tool IDs for the `--tools` flag:
**Preferred:** `claude-code`, `cursor`, `windsurf`
Run `npx bmad-method install` interactively once to see the full current list of supported tools, or check the [platform codes configuration](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/tools/cli/installers/lib/ide/platform-codes.yaml).
## Installation Modes
| Mode | Description | Example |
|------|-------------|---------|
| Fully non-interactive | Provide all flags to skip all prompts | `npx bmad-method install --directory . --modules bmm --tools claude-code --yes` |
| Semi-interactive | Provide some flags; BMad prompts for the rest | `npx bmad-method install --directory . --modules bmm` |
| Defaults only | Accept all defaults with `-y` | `npx bmad-method install --yes` |
| Without tools | Skip tool/IDE configuration | `npx bmad-method install --modules bmm --tools none` |
## Examples
### CI/CD Pipeline Installation
```bash
#!/bin/bash
# install-bmad.sh
npx bmad-method install \
--directory "${GITHUB_WORKSPACE}" \
--modules bmm \
--tools claude-code \
--user-name "CI Bot" \
--communication-language English \
--document-output-language English \
--output-folder _bmad-output \
--yes
```
### Update Existing Installation
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--action update \
--modules bmm,bmb,custom-module
```
### Quick Update (Preserve Settings)
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--action quick-update
```
### Installation with Custom Content
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--custom-content ~/my-custom-module,~/another-module \
--tools claude-code
```
## What You Get
- A fully configured `_bmad/` directory in your project
- Compiled agents and workflows for your selected modules and tools
- A `_bmad-output/` folder for generated artifacts
## Validation and Error Handling
BMad validates all provided flags:
- **Directory** — Must be a valid path with write permissions
- **Modules** — Warns about invalid module IDs (but won't fail)
- **Tools** — Warns about invalid tool IDs (but won't fail)
- **Custom Content** — Each path must contain a valid `module.yaml` file
- **Action** — Must be one of: `install`, `update`, `quick-update`, `compile-agents`
Invalid values will either:
1. Show an error and exit (for critical options like directory)
2. Show a warning and skip (for optional items like custom content)
3. Fall back to interactive prompts (for missing required values)
:::tip[Best Practices]
- Use absolute paths for `--directory` to avoid ambiguity
- Test flags locally before using in CI/CD pipelines
- Combine with `-y` for truly unattended installations
- Use `--debug` if you encounter issues during installation
:::
## Troubleshooting
### Installation fails with "Invalid directory"
- The directory path must exist (or its parent must exist)
- You need write permissions
- The path must be absolute or correctly relative to the current directory
### Module not found
- Verify the module ID is correct
- External modules must be available in the registry
### Custom content path invalid
Ensure each custom content path:
- Points to a directory
- Contains a `module.yaml` file in the root
- Has a `code` field in the `module.yaml`
:::note[Still stuck?]
Run with `--debug` for detailed output, try interactive mode to isolate the issue, or report at <https://github.com/bmad-code-org/BMAD-METHOD/issues>.
:::

View File

@ -1,76 +1,123 @@
---
title: "Quick Fixes"
description: How to make quick fixes and ad-hoc changes
sidebar:
order: 5
---
Use the **DEV agent** directly for bug fixes, refactorings, or small targeted changes that don't require the full BMad method or Quick Flow.
Use the **DEV agent** directly for bug fixes, refactorings, or small targeted changes that don't require the full BMad Method or Quick Flow.
## When to Use This
- Simple bug fixes
- Small refactorings and changes that don't need extensive ideation, planning, or architectural shifts
- Larger refactorings or improvement with built in tool planning and execution mode combination, or better yet use quick flow
- Learning about your codebase
- Bug fixes with a clear, known cause
- Small refactorings (rename, extract, restructure) contained within a few files
- Minor feature tweaks or configuration changes
- Exploratory work to understand an unfamiliar codebase
:::note[Prerequisites]
- BMad Method installed (`npx bmad-method install`)
- An AI-powered IDE (Claude Code, Cursor, Windsurf, or similar)
:::
## Choose Your Approach
| Situation | Agent | Why |
| --- | --- | --- |
| Fix a specific bug or make a small, scoped change | **DEV agent** | Jumps straight into implementation without planning overhead |
| Change touches several files or you want a written plan first | **Quick Flow Solo Dev** | Creates a quick-spec before implementation so the agent stays aligned to your standards |
If you are unsure, start with the DEV agent. You can always escalate to Quick Flow if the change grows.
## Steps
### 1. Load an Agent
### 1. Load the DEV Agent
For quick fixes, you can use:
Start a **fresh chat** in your AI IDE and load the DEV agent with its slash command:
- **DEV agent** - For implementation-focused work
- **Quick Flow Solo Dev** - For slightly larger changes that still need a quick-spec to keep the agent aligned to planning and standards
```text
/bmad-agent-bmm-dev
```
This loads the agent's persona and capabilities into the session. If you decide you need Quick Flow instead, load the **Quick Flow Solo Dev** agent in a fresh chat:
```text
/bmad-agent-bmm-quick-flow-solo-dev
```
Once the Solo Dev agent is loaded, describe your change and ask it to create a **quick-spec**. The agent drafts a lightweight spec capturing what you want to change and how. After you approve the quick-spec, tell the agent to start the **Quick Flow dev cycle** -- it will implement the change, run tests, and perform a self-review, all guided by the spec you just approved.
:::tip[Fresh Chats]
Always start a new chat session when loading an agent. Reusing a session from a previous workflow can cause context conflicts.
:::
### 2. Describe the Change
Simply tell the agent what you need:
Tell the agent what you need in plain language. Be specific about the problem and, if you know it, where the relevant code lives.
```
Fix the login validation bug that allows empty passwords
```
:::note[Example Prompts]
**Bug fix** -- "Fix the login validation bug that allows empty passwords. The validation logic is in `src/auth/validate.ts`."
or
**Refactoring** -- "Refactor the UserService to use async/await instead of callbacks."
```
Refactor the UserService to use async/await instead of callbacks
```
**Configuration change** -- "Update the CI pipeline to cache node_modules between runs."
**Dependency update** -- "Upgrade the express dependency to the latest v5 release and fix any breaking changes."
:::
You don't need to provide every detail. The agent will read the relevant source files and ask clarifying questions when needed.
### 3. Let the Agent Work
The agent will:
- Analyze the relevant code
- Propose a solution
- Implement the change
- Run tests (if available)
- Read and analyze the relevant source files
- Propose a solution and explain its reasoning
- Implement the change across the affected files
- Run your project's test suite if one exists
### 4. Review and Commit
If your project has tests, the agent runs them automatically after making changes and iterates until tests pass. For projects without a test suite, verify the change manually (run the app, hit the endpoint, check the output).
Review the changes made and commit when satisfied.
### 4. Review and Verify
Before committing, review what changed:
- Read through the diff to confirm the change matches your intent
- Run the application or tests yourself to double-check
- If something looks wrong, tell the agent what to fix -- it can iterate in the same session
Once satisfied, commit the changes with a clear message describing the fix.
:::caution[If Something Breaks]
If a committed change causes unexpected issues, use `git revert HEAD` to undo the last commit cleanly. Then start a fresh chat with the DEV agent to try a different approach.
:::
## Learning Your Codebase
This approach is also excellent for exploring unfamiliar code:
The DEV agent is also useful for exploring unfamiliar code. Load it in a fresh chat and ask questions:
```
Explain how the authentication system works in this codebase
```
:::note[Example Prompts]
"Explain how the authentication system works in this codebase."
```
Show me where error handling happens in the API layer
```
"Show me where error handling happens in the API layer."
LLMs are excellent at interpreting and analyzing code, whether it was AI-generated or not. Use the agent to:
"What does the `ProcessOrder` function do and what calls it?"
:::
- Learn about your project
- Understand how things are built
- Explore unfamiliar parts of the codebase
Use the agent to learn about your project, understand how components connect, and explore unfamiliar areas before making changes.
## What You Get
- Modified source files with the fix or refactoring applied
- Passing tests (if your project has a test suite)
- A clean commit describing the change
No planning artifacts are produced -- that's the point of this approach.
## When to Upgrade to Formal Planning
Consider using Quick Flow or full BMad Method when:
Consider using [Quick Flow](../explanation/quick-flow.md) or the full BMad Method when:
- The change affects multiple files or systems
- You're unsure about the scope
- The fix keeps growing in complexity
- You need documentation for the change
- The change affects multiple systems or requires coordinated updates across many files
- You are unsure about the scope and need a spec to think it through
- The fix keeps growing in complexity as you work on it
- You need documentation or architectural decisions recorded for the team

View File

@ -1,14 +1,19 @@
---
title: "Document Sharding Guide"
description: Split large markdown files into smaller organized files for better context management
sidebar:
order: 8
---
Use the `shard-doc` tool if you need to split large markdown files into smaller, organized files for better context management.
This is no longer recommended, and soon with updated workflows and most major llms and tools supporting sub processes this will be unnecessary.
:::caution[Deprecated]
This is no longer recommended, and soon with updated workflows and most major LLMs and tools supporting subprocesses this will be unnecessary.
:::
## When to Use This
Only use this if you notice your chosen tool / model combination are failing to load and read all the documents as input when needed.
Only use this if you notice your chosen tool / model combination is failing to load and read all the documents as input when needed.
## What is Document Sharding?
@ -16,7 +21,7 @@ Document sharding splits large markdown files into smaller, organized files base
### Architecture
```
```text
Before Sharding:
docs/
└── PRD.md (large 50k token file)
@ -36,12 +41,12 @@ docs/
### 1. Run the Shard-Doc Tool
```bash
/bmad:core:tools:shard-doc
/bmad-shard-doc
```
### 2. Follow the Interactive Process
```
```text
Agent: Which document would you like to shard?
User: docs/PRD.md

View File

@ -1,6 +1,8 @@
---
title: "How to Upgrade to v6"
description: Migrate from BMad v4 to v6
sidebar:
order: 3
---
Use the BMad installer to upgrade from v4 to v6, which includes automatic detection of legacy installations and migration assistance.
@ -20,7 +22,7 @@ Use the BMad installer to upgrade from v4 to v6, which includes automatic detect
### 1. Run the Installer
Follow the [Installer Instructions](/docs/how-to/install-bmad.md).
Follow the [Installer Instructions](./install-bmad.md).
### 2. Handle Legacy Installation
@ -29,7 +31,7 @@ When v4 is detected, you can:
- Allow the installer to back up and remove `.bmad-method`
- Exit and handle cleanup manually
If you named your bmad method folder something else - you will need to manual remove the folder yourself.
If you named your bmad method folder something else - you will need to manually remove the folder yourself.
### 3. Clean Up IDE Commands
@ -63,16 +65,16 @@ If you have stories created or implemented:
**v6 unified structure:**
```
```text
your-project/
── _bmad/ # Single installation folder
├── _config/ # Your customizations
│ └── agents/ # Agent customization files
├── core/ # Universal core framework
├── bmm/ # BMad Method module
├── bmb/ # BMad Builder
└── cis/ # Creative Intelligence Suite
── _bmad-output/ # Output folder (was doc folder in v4)
── _bmad/ # Single installation folder
├── _config/ # Your customizations
│ └── agents/ # Agent customization files
├── core/ # Universal core framework
├── bmm/ # BMad Method module
├── bmb/ # BMad Builder
└── cis/ # Creative Intelligence Suite
── _bmad-output/ # Output folder (was doc folder in v4)
```
## Module Migration

View File

@ -1,19 +1,18 @@
---
title: Welcome to the BMad Method
description: AI-driven development framework with specialized agents, guided workflows, and intelligent planning
---
The BMad Method (**B**reakthrough **M**ethod of **A**gile AI **D**riven Development) is an AI-driven development framework that helps you build software through the whole process from ideation and planning all the way through agentic implementation. It provides specialized AI agents, guided workflows, and intelligent planning that adapts to your project's complexity, whether you're fixing a bug or building an enterprise platform.
If you're comfortable working with AI coding assistants like Claude, Cursor, or GitHub Copilot, you're ready to get started.
---
## New Here? Start with a Tutorial
The fastest way to understand BMad is to try it.
- **[Get Started with BMad](/docs/tutorials/getting-started.md)** — Install and understand how BMad works
- **[Workflow Map](/docs/reference/workflow-map.md)** — Visual overview of BMM phases, workflows, and context management.
- **[Get Started with BMad](./tutorials/getting-started.md)** — Install and understand how BMad works
- **[Workflow Map](./reference/workflow-map.md)** — Visual overview of BMM phases, workflows, and context management.
## How to Use These Docs
@ -26,8 +25,6 @@ These docs are organized into four sections based on what you're trying to do:
| **Explanation** | Understanding-oriented. Deep dives into concepts and architecture. Read when you want to know *why*. |
| **Reference** | Information-oriented. Technical specifications for agents, workflows, and configuration. |
---
## What You'll Need
BMad works with any AI coding assistant that supports custom system prompts or project context. Popular options include:
@ -35,12 +32,11 @@ BMad works with any AI coding assistant that supports custom system prompts or p
- **[Claude Code](https://code.claude.com)** — Anthropic's CLI tool (recommended)
- **[Cursor](https://cursor.sh)** — AI-first code editor
- **[Windsurf](https://codeium.com/windsurf)** — Codeium's AI IDE
- **[Kiro](https://kiro.dev)** — Amazon's AI-powered IDE
- **[Roo Code](https://roocode.com)** — VS Code extension
You should be comfortable with basic software development concepts like version control, project structure, and agile workflows. No prior experience with BMad-style agent systems is required—that's what these docs are for.
---
## Join the Community
Get help, share what you're building, or contribute to BMad:
@ -49,8 +45,6 @@ Get help, share what you're building, or contribute to BMad:
- **[GitHub](https://github.com/bmad-code-org/BMAD-METHOD)** — Source code, issues, and contributions
- **[YouTube](https://www.youtube.com/@BMadCode)** — Video tutorials and walkthroughs
---
## Next Step
Ready to dive in? **[Get Started with BMad](/docs/tutorials/getting-started.md)** and build your first project.
Ready to dive in? **[Get Started with BMad](./tutorials/getting-started.md)** and build your first project.

View File

@ -1,314 +0,0 @@
---
title: Non-Interactive Installation
description: Install BMAD using command-line flags for CI/CD pipelines and automated deployments
---
# Non-Interactive Installation
BMAD now supports non-interactive installation through command-line flags. This is particularly useful for:
- Automated deployments and CI/CD pipelines
- Scripted installations
- Batch installations across multiple projects
- Quick installations with known configurations
## Installation Modes
### 1. Fully Interactive (Default)
Run without any flags to use the traditional interactive prompts:
```bash
npx bmad-method install
```
### 2. Fully Non-Interactive
Provide all required flags to skip all prompts:
```bash
npx bmad-method install \
--directory /path/to/project \
--modules bmm,bmb \
--tools claude-code,cursor \
--user-name "John Doe" \
--communication-language English \
--document-output-language English \
--output-folder _bmad-output
```
### 3. Semi-Interactive (Graceful Fallback)
Provide some flags and let BMAD prompt for the rest:
```bash
npx bmad-method install \
--directory /path/to/project \
--modules bmm
```
In this case, BMAD will:
- Use the provided directory and modules
- Prompt for tool selection
- Prompt for core configuration
### 4. Quick Install with Defaults
Use the `-y` or `--yes` flag to accept all defaults:
```bash
npx bmad-method install --yes
```
This will:
- Install to the current directory
- Skip custom content prompts
- Use default values for all configuration
- Use previously configured tools (or skip tool configuration if none exist)
### 5. Install Without Tools
To skip tool/IDE configuration entirely:
**Option 1: Use --tools none**
```bash
npx bmad-method install --directory ~/myapp --modules bmm --tools none
```
**Option 2: Use --yes flag (if no tools were previously configured)**
```bash
npx bmad-method install --yes
```
**Option 3: Omit --tools and select "None" in the interactive prompt**
```bash
npx bmad-method install --directory ~/myapp --modules bmm
# Then select "⚠ None - I am not installing any tools" when prompted
```
## Available Flags
### Installation Options
| Flag | Description | Example |
|------|-------------|---------|
| `--directory <path>` | Installation directory | `--directory ~/projects/myapp` |
| `--modules <modules>` | Comma-separated module IDs | `--modules bmm,bmb` |
| `--tools <tools>` | Comma-separated tool/IDE IDs (use "none" to skip) | `--tools claude-code,cursor` or `--tools none` |
| `--custom-content <paths>` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | Action for existing installations | `--action quick-update` |
### Core Configuration
| Flag | Description | Default |
|------|-------------|---------|
| `--user-name <name>` | Name for agents to use | System username |
| `--communication-language <lang>` | Agent communication language | English |
| `--document-output-language <lang>` | Document output language | English |
| `--output-folder <path>` | Output folder path | _bmad-output |
### Other Options
| Flag | Description |
|------|-------------|
| `-y, --yes` | Accept all defaults and skip prompts |
| `-d, --debug` | Enable debug output for manifest generation |
## Action Types
When working with existing installations, use the `--action` flag:
- `install` - Fresh installation (default for new directories)
- `update` - Modify existing installation (change modules/config)
- `quick-update` - Refresh installation without changing configuration
- `compile-agents` - Recompile agents with customizations only
Example:
```bash
npx bmad-method install --action quick-update
```
## Module IDs
Available module IDs for the `--modules` flag:
### Core Modules
- `bmm` - BMad Method Master
- `bmb` - BMad Builder
### External Modules
Check the [BMad registry](https://github.com/bmad-code-org) for available external modules.
## Tool/IDE IDs
Available tool IDs for the `--tools` flag:
- `claude-code` - Claude Code CLI
- `cursor` - Cursor IDE
- `windsurf` - Windsurf IDE
- `vscode` - Visual Studio Code
- `jetbrains` - JetBrains IDEs
- And more...
Run the interactive installer once to see all available tools.
## Examples
### Basic Installation
Install BMM module with Claude Code:
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--tools claude-code \
--user-name "Development Team"
```
### Installation Without Tools
Install without configuring any tools/IDEs:
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--tools none \
--user-name "Development Team"
```
### Full Installation with Multiple Modules
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm,bmb \
--tools claude-code,cursor \
--user-name "John Doe" \
--communication-language English \
--document-output-language English \
--output-folder _output
```
### Update Existing Installation
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--action update \
--modules bmm,bmb,custom-module
```
### Quick Update (Preserve Settings)
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--action quick-update
```
### Installation with Custom Content
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--custom-content ~/my-custom-module,~/another-module \
--tools claude-code
```
### CI/CD Pipeline Installation
```bash
#!/bin/bash
# install-bmad.sh
npx bmad-method install \
--directory "${GITHUB_WORKSPACE}" \
--modules bmm \
--tools claude-code \
--user-name "CI Bot" \
--communication-language English \
--document-output-language English \
--output-folder _bmad-output \
--yes
```
## Environment-Specific Installations
### Development Environment
```bash
npx bmad-method install \
--directory . \
--modules bmm,bmb \
--tools claude-code,cursor \
--user-name "${USER}"
```
### Production Environment
```bash
npx bmad-method install \
--directory /opt/app \
--modules bmm \
--tools claude-code \
--user-name "Production Team" \
--output-folder /var/bmad-output
```
## Validation and Error Handling
BMAD validates all provided flags:
- **Directory**: Must be a valid path with write permissions
- **Modules**: Will warn about invalid module IDs (but won't fail)
- **Tools**: Will warn about invalid tool IDs (but won't fail)
- **Custom Content**: Each path must contain a valid `module.yaml` file
- **Action**: Must be one of: install, update, quick-update, compile-agents
Invalid values will either:
1. Show an error and exit (for critical options like directory)
2. Show a warning and skip (for optional items like custom content)
3. Fall back to interactive prompts (for missing required values)
## Tips and Best Practices
1. **Use absolute paths** for `--directory` to avoid ambiguity
2. **Test flags locally** before using in CI/CD pipelines
3. **Combine with `-y`** for truly unattended installations
4. **Check module availability** by running the interactive installer once
5. **Use `--debug`** flag if you encounter issues during installation
6. **Skip tool configuration** with `--tools none` for server/CI environments where IDEs aren't needed
7. **Partial flags are OK** - Omit flags and let BMAD prompt for missing values interactively
## Troubleshooting
### Installation fails with "Invalid directory"
Check that:
- The directory path exists or its parent exists
- You have write permissions
- The path is absolute or correctly relative to current directory
### Module not found
- Verify the module ID is correct (check available modules in interactive mode)
- External modules may need to be available in the registry
### Custom content path invalid
Ensure each custom content path:
- Points to a directory
- Contains a `module.yaml` file in the root
- Has a `code` field in the `module.yaml`
## Feedback and Issues
If you encounter any issues with non-interactive installation:
1. Run with `--debug` flag for detailed output
2. Try the interactive mode to verify the issue
3. Report issues on GitHub: <https://github.com/bmad-code-org/BMAD-METHOD/issues>

View File

@ -1,12 +1,18 @@
---
title: Agents
description: Default BMM agents with their menu triggers and primary workflows
sidebar:
order: 2
---
This page lists the default BMM (Agile suite) agents that install with BMAD Method, along with their menu triggers and primary workflows.
## Default Agents
This page lists the default BMM (Agile suite) agents that install with BMad Method, along with their menu triggers and primary workflows.
## Notes
Notes:
- Triggers are the short menu codes (e.g., `CP`) and fuzzy matches shown in each agent menu.
- Slash commands are generated separately. See `docs/reference/commands.md` for the slash command list and where they are defined.
- Slash commands are generated separately. See [Commands](./commands.md) for the slash command list and where they are defined.
- QA (Quinn) is the lightweight test automation agent in BMM. The full Test Architect (TEA) lives in its own module.
| Agent | Triggers | Primary workflows |

View File

@ -1,34 +1,131 @@
---
title: Commands
description: How BMAD commands are generated and where to find them.
description: Reference for BMad slash commands — what they are, how they work, and where to find them.
sidebar:
order: 3
---
# Commands
Slash commands are pre-built prompts that load agents, run workflows, or execute tasks inside your IDE. The BMad installer generates them from your installed modules at install time. If you later add, remove, or change modules, re-run the installer to keep commands in sync (see [Troubleshooting](#troubleshooting)).
BMAD slash commands are generated by the installer for your IDE and **reflect the modules you have installed**.
That means the authoritative list lives **in your project**, not in a static docs page.
## Commands vs. Agent Menu Triggers
## How to Discover Commands (Recommended)
BMad offers two ways to start work, and they serve different purposes.
- Type `/bmad` in your IDE and use autocomplete to browse agents/workflows.
- Run `/bmad-help` to get guided next steps and context-aware recommendations.
| Mechanism | How you invoke it | What happens |
| --- | --- | --- |
| **Slash command** | Type `/bmad-...` in your IDE | Directly loads an agent, runs a workflow, or executes a task |
| **Agent menu trigger** | Load an agent first, then type a short code (e.g. `DS`) | The agent interprets the code and starts the matching workflow while staying in character |
## Where Commands Are Generated
Agent menu triggers require an active agent session. Use slash commands when you know which workflow you want. Use triggers when you are already working with an agent and want to switch tasks without leaving the conversation.
The installer writes command files into your project (example paths for Claude Code):
## How Commands Are Generated
- `.claude/commands/bmad/<module>/agents/`
- `.claude/commands/bmad/<module>/workflows/`
When you run `npx bmad-method install`, the installer reads the manifests for every selected module and writes one command file per agent, workflow, task, and tool. Each file is a short markdown prompt that instructs the AI to load the corresponding source file and follow its instructions.
These folders are the **canonical, project-specific command list**.
The installer uses templates for each command type:
## Common Commands
| Command type | What the generated file does |
| --- | --- |
| **Agent launcher** | Loads the agent persona file, activates its menu, and stays in character |
| **Workflow command** | Loads the workflow engine (`workflow.xml`) and passes the workflow config |
| **Task command** | Loads a standalone task file and follows its instructions |
| **Tool command** | Loads a standalone tool file and follows its instructions |
- `/bmad-help` - Interactive help and next-step guidance
- `/bmad:<module>:agents:<agent>` - Load an agent (e.g. `/bmad:bmm:agents:dev`)
- `/bmad:<module>:workflows:<workflow>` - Run a workflow (e.g. `/bmad:bmm:workflows:create-prd`)
:::note[Re-running the installer]
If you add or remove modules, run the installer again. It regenerates all command files to match your current module selection.
:::
## Why This Page Is Short
## Where Command Files Live
BMAD is modular, so the exact commands vary by install.
Use your IDE's autocomplete or the generated command folders above to see *everything* available.
The installer writes command files into an IDE-specific directory inside your project. The exact path depends on which IDE you selected during installation.
| IDE / CLI | Command directory |
| --- | --- |
| Claude Code | `.claude/commands/` |
| Cursor | `.cursor/commands/` |
| Windsurf | `.windsurf/workflows/` |
| Other IDEs | See the installer output for the target path |
All IDEs receive a flat set of command files in their command directory. For example, a Claude Code installation looks like:
```text
.claude/commands/
├── bmad-agent-bmm-dev.md
├── bmad-agent-bmm-pm.md
├── bmad-bmm-create-prd.md
├── bmad-editorial-review-prose.md
├── bmad-help.md
└── ...
```
The filename determines the slash command name in your IDE. For example, the file `bmad-agent-bmm-dev.md` registers the command `/bmad-agent-bmm-dev`.
## How to Discover Your Commands
Type `/bmad` in your IDE and use autocomplete to browse available commands.
Run `/bmad-help` for context-aware guidance on your next step.
:::tip[Quick discovery]
The generated command folders in your project are the canonical list. Open them in your file explorer to see every command with its description.
:::
## Command Categories
### Agent Commands
Agent commands load a specialized AI persona with a defined role, communication style, and menu of workflows. Once loaded, the agent stays in character and responds to menu triggers.
| Example command | Agent | Role |
| --- | --- | --- |
| `/bmad-agent-bmm-dev` | Amelia (Developer) | Implements stories with strict adherence to specs |
| `/bmad-agent-bmm-pm` | John (Product Manager) | Creates and validates PRDs |
| `/bmad-agent-bmm-architect` | Winston (Architect) | Designs system architecture |
| `/bmad-agent-bmm-sm` | Bob (Scrum Master) | Manages sprints and stories |
See [Agents](./agents.md) for the full list of default agents and their triggers.
### Workflow Commands
Workflow commands run a structured, multi-step process without loading an agent persona first. They load the workflow engine and pass a specific workflow configuration.
| Example command | Purpose |
| --- | --- |
| `/bmad-bmm-create-prd` | Create a Product Requirements Document |
| `/bmad-bmm-create-architecture` | Design system architecture |
| `/bmad-bmm-dev-story` | Implement a story |
| `/bmad-bmm-code-review` | Run a code review |
| `/bmad-bmm-quick-spec` | Define an ad-hoc change (Quick Flow) |
See [Workflow Map](./workflow-map.md) for the complete workflow reference organized by phase.
### Task and Tool Commands
Tasks and tools are standalone operations that do not require an agent or workflow context.
| Example command | Purpose |
| --- | --- |
| `/bmad-help` | Context-aware guidance and next-step recommendations |
| `/bmad-shard-doc` | Split a large markdown file into smaller sections |
| `/bmad-index-docs` | Index project documentation |
| `/bmad-editorial-review-prose` | Review document prose quality |
## Naming Convention
Command names follow a predictable pattern.
| Pattern | Meaning | Example |
| --- | --- | --- |
| `bmad-agent-<module>-<name>` | Agent launcher | `bmad-agent-bmm-dev` |
| `bmad-<module>-<workflow>` | Workflow command | `bmad-bmm-create-prd` |
| `bmad-<name>` | Core task or tool | `bmad-help` |
Module codes: `bmm` (Agile suite), `bmb` (Builder), `tea` (Test Architect), `cis` (Creative Intelligence), `gds` (Game Dev Studio). See [Modules](./modules.md) for descriptions.
## Troubleshooting
**Commands not appearing after install.** Restart your IDE or reload the window. Some IDEs cache the command list and require a refresh to pick up new files.
**Expected commands are missing.** The installer only generates commands for modules you selected. Run `npx bmad-method install` again and verify your module selection. Check that the command files exist in the expected directory.
**Commands from a removed module still appear.** The installer does not delete old command files automatically. Remove the stale files from your IDE's command directory, or delete the entire command directory and re-run the installer for a clean set.

View File

@ -1,5 +1,8 @@
---
title: Official Modules
description: Add-on modules for building custom agents, creative intelligence, game development, and testing
sidebar:
order: 4
---
BMad extends through official modules that you select during installation. These add-on modules provide specialized agents, workflows, and tasks for specific domains beyond the built-in core and BMM (Agile suite).

View File

@ -1,21 +1,106 @@
---
title: Testing Options
description: Comparing the built-in QA agent (Quinn) with the Test Architect (TEA) module for test automation.
sidebar:
order: 5
---
# Testing Options
BMad provides two testing paths: a built-in QA agent for fast test generation and an installable Test Architect module for enterprise-grade test strategy.
BMad provides a built-in QA agent for quick test automation and a separate Test Architect (TEA) module for advanced testing.
## Which Should You Use?
## Built-in QA (Quinn)
| Factor | Quinn (Built-in QA) | TEA Module |
| --- | --- | --- |
| **Best for** | Small-medium projects, quick coverage | Large projects, regulated or complex domains |
| **Setup** | Nothing to install -- included in BMM | Install separately via `npx bmad-method install` |
| **Approach** | Generate tests fast, iterate later | Plan first, then generate with traceability |
| **Test types** | API and E2E tests | API, E2E, ATDD, NFR, and more |
| **Strategy** | Happy path + critical edge cases | Risk-based prioritization (P0-P3) |
| **Workflow count** | 1 (Automate) | 9 (design, ATDD, automate, review, trace, and others) |
Use the built-in QA agent for fast, straightforward test coverage:
:::tip[Start with Quinn]
Most projects should start with Quinn. If you later need test strategy, quality gates, or requirements traceability, install TEA alongside it.
:::
- Trigger: `QA` or `bmad-bmm-qa-automate`
- Best for: small projects, quick coverage, standard patterns
## Built-in QA Agent (Quinn)
Quinn is the built-in QA agent in the BMM (Agile suite) module. It generates working tests quickly using your project's existing test framework -- no configuration or additional installation required.
**Trigger:** `QA` or `bmad-bmm-qa-automate`
### What Quinn Does
Quinn runs a single workflow (Automate) that walks through five steps:
1. **Detect test framework** -- scans `package.json` and existing test files for your framework (Jest, Vitest, Playwright, Cypress, or any standard runner). If none exists, analyzes the project stack and suggests one.
2. **Identify features** -- asks what to test or auto-discovers features in the codebase.
3. **Generate API tests** -- covers status codes, response structure, happy path, and 1-2 error cases.
4. **Generate E2E tests** -- covers user workflows with semantic locators and visible-outcome assertions.
5. **Run and verify** -- executes the generated tests and fixes failures immediately.
Quinn produces a test summary saved to your project's implementation artifacts folder.
### Test Patterns
Generated tests follow a "simple and maintainable" philosophy:
- **Standard framework APIs only** -- no external utilities or custom abstractions
- **Semantic locators** for UI tests (roles, labels, text rather than CSS selectors)
- **Independent tests** with no order dependencies
- **No hardcoded waits or sleeps**
- **Clear descriptions** that read as feature documentation
:::note[Scope]
Quinn generates tests only. For code review and story validation, use the Code Review workflow (`CR`) instead.
:::
### When to Use Quinn
- Quick test coverage for a new or existing feature
- Beginner-friendly test automation without advanced setup
- Standard test patterns that any developer can read and maintain
- Small-medium projects where comprehensive test strategy is unnecessary
## Test Architect (TEA) Module
TEA is a standalone module with advanced testing workflows (test design, ATDD, automate, review, trace, NFR assessment).
TEA is a standalone module that provides an expert agent (Murat) and nine structured workflows for enterprise-grade testing. It goes beyond test generation into test strategy, risk-based planning, quality gates, and requirements traceability.
- Documentation: <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/>
- Install: `npx bmad-method@alpha install` and select the TEA module
- **Documentation:** [TEA Module Docs](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/)
- **Install:** `npx bmad-method install` and select the TEA module
- **npm:** [`bmad-method-test-architecture-enterprise`](https://www.npmjs.com/package/bmad-method-test-architecture-enterprise)
### What TEA Provides
| Workflow | Purpose |
| --- | --- |
| Test Design | Create a comprehensive test strategy tied to requirements |
| ATDD | Acceptance-test-driven development with stakeholder criteria |
| Automate | Generate tests with advanced patterns and utilities |
| Test Review | Validate test quality and coverage against strategy |
| Traceability | Map tests back to requirements for audit and compliance |
| NFR Assessment | Evaluate non-functional requirements (performance, security) |
| CI Setup | Configure test execution in continuous integration pipelines |
| Framework Scaffolding | Set up test infrastructure and project structure |
| Release Gate | Make data-driven go/no-go release decisions |
TEA also supports P0-P3 risk-based prioritization and optional integrations with Playwright Utils and MCP tooling.
### When to Use TEA
- Projects that require requirements traceability or compliance documentation
- Teams that need risk-based test prioritization across many features
- Enterprise environments with formal quality gates before release
- Complex domains where test strategy must be planned before tests are written
- Projects that have outgrown Quinn's single-workflow approach
## How Testing Fits into Workflows
Quinn's Automate workflow appears in Phase 4 (Implementation) of the BMad Method workflow map. A typical sequence:
1. Implement a story with the Dev workflow (`DS`)
2. Generate tests with Quinn (`QA`) or TEA's Automate workflow
3. Validate implementation with Code Review (`CR`)
Quinn works directly from source code without loading planning documents (PRD, architecture). TEA workflows can integrate with upstream planning artifacts for traceability.
For more on where testing fits in the overall process, see the [Workflow Map](./workflow-map.md).

View File

@ -1,19 +1,23 @@
---
title: "Workflow Map"
description: Visual reference for BMad Method workflow phases and outputs
sidebar:
order: 1
---
The BMad Method (BMM) is a module in the BMad Ecosystem, targeted at following the best practices of context engineering and planning. AI agents work best with clear, structured context. The BMM system builds that context progressively across 4 distinct phases - each phase, and multiple workflows optionally within each phase, produce documents that inform the next, so agents always know what to build and why.
The rationale and concepts come from agile methodologies that have been used across the industry with great success as a mental framework.
If at anytime you are unsure what to do, the `/bmad-help` command will help you stay on track or know what to do next. You can always refer to this for reference also - but /bmad-help is fully interactive and much quicker if you have already installed the BMadMethod. Additionally, if you are using different modules that have extended the BMad Method or added other complimentary non extension modules - the /bmad-help evolves to know all that is available to give you the best in the moment advice.
If at any time you are unsure what to do, the `/bmad-help` command will help you stay on track or know what to do next. You can always refer to this for reference also - but /bmad-help is fully interactive and much quicker if you have already installed the BMad Method. Additionally, if you are using different modules that have extended the BMad Method or added other complementary non-extension modules - the /bmad-help evolves to know all that is available to give you the best in-the-moment advice.
Final important note: Every workflow below can be run directly with your tool of choice via slash command or by loading an agent first and using the entry from the agents menu.
<iframe src="/workflow-map-diagram.html" width="100%" height="100%" frameborder="0" style="border-radius: 8px; border: 1px solid #334155; min-height: 900px;"></iframe>
<iframe src="/workflow-map-diagram.html" title="BMad Method Workflow Map Diagram" width="100%" height="100%" style="border-radius: 8px; border: 1px solid #334155; min-height: 900px;"></iframe>
*[Interactive diagram - hover over outputs to see artifact flows]*
<p style="font-size: 0.8rem; text-align: right; margin-top: -0.5rem; margin-bottom: 1rem;">
<a href="/workflow-map-diagram.html" target="_blank" rel="noopener noreferrer">Open diagram in new tab ↗</a>
</p>
## Phase 1: Analysis (Optional)
@ -21,7 +25,7 @@ Explore the problem space and validate ideas before committing to planning.
| Workflow | Purpose | Produces |
| ---------------------- | -------------------------------------------------------------------------- | ------------------------- |
| `brainstorm` | Brainstorm Project Ideas with guided facilitation of a brainstorming coach | `brainstorming-report.md` |
| `brainstorming` | Brainstorm Project Ideas with guided facilitation of a brainstorming coach | `brainstorming-report.md` |
| `research` | Validate market, technical, or domain assumptions | Research findings |
| `create-product-brief` | Capture strategic vision | `product-brief.md` |

View File

@ -37,7 +37,7 @@ BMad helps you build software through guided workflows with specialized AI agent
| 3 | Solutioning | Design architecture *(BMad Method/Enterprise only)* |
| 4 | Implementation | Build epic by epic, story by story |
**[Open the Workflow Map](/docs/reference/workflow-map.md)** to explore phases, workflows, and context management.
**[Open the Workflow Map](../reference/workflow-map.md)** to explore phases, workflows, and context management.
Based on your project's complexity, BMad offers three planning tracks:
@ -153,7 +153,7 @@ You've learned the foundation of building with BMad:
Your project now has:
```
```text
your-project/
├── _bmad/ # BMad configuration
├── _bmad-output/

View File

@ -12,11 +12,7 @@ export default [
'coverage/**',
'**/*.min.js',
'test/template-test-generator/**',
'test/template-test-generator/**/*.js',
'test/template-test-generator/**/*.md',
'test/fixtures/**',
'test/fixtures/**/*.yaml',
'_bmad/**',
'_bmad*/**',
// Build output
'build/**',
@ -36,6 +32,10 @@ export default [
'tools/template-test-generator/test-scenarios/**',
'src/modules/*/sub-modules/**',
'.bundler-temp/**',
// Augment vendor config — not project code, naming conventions
// are dictated by Augment and can't be changed, so exclude
// the entire directory from linting
'.augment/**',
],
},
@ -83,7 +83,7 @@ export default [
// CLI scripts under tools/** and test/**
{
files: ['tools/**/*.js', 'tools/**/*.mjs', 'test/**/*.js'],
files: ['tools/**/*.js', 'tools/**/*.mjs', 'test/**/*.js', 'test/**/*.mjs'],
rules: {
// Allow CommonJS patterns for Node CLI scripts
'unicorn/prefer-module': 'off',
@ -114,17 +114,6 @@ export default [
},
},
// Module installer scripts use CommonJS for compatibility
{
files: ['**/_module-installer/**/*.js'],
rules: {
// Allow CommonJS patterns for installer scripts
'unicorn/prefer-module': 'off',
'n/no-missing-require': 'off',
'n/no-unpublished-require': 'off',
},
},
// ESLint config file should not be checked for publish-related Node rules
{
files: ['eslint.config.mjs'],

856
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "bmad-method",
"version": "6.0.0-Beta.7",
"version": "6.0.0-Beta.8",
"description": "Breakthrough Method of Agile AI-driven Development",
"keywords": [
"agile",
@ -25,13 +25,11 @@
},
"scripts": {
"bmad:install": "node tools/cli/bmad-cli.js install",
"bundle": "node tools/cli/bundlers/bundle-web.js all",
"docs:build": "node tools/build-docs.mjs",
"docs:dev": "astro dev --root website",
"docs:fix-links": "node tools/fix-doc-links.js",
"docs:preview": "astro preview --root website",
"docs:validate-links": "node tools/validate-doc-links.js",
"flatten": "node tools/flattener/main.js",
"format:check": "prettier --check \"**/*.{js,cjs,mjs,json,yaml}\"",
"format:fix": "prettier --write \"**/*.{js,cjs,mjs,json,yaml}\"",
"format:fix:staged": "prettier --write",
@ -41,9 +39,10 @@
"lint:md": "markdownlint-cli2 \"**/*.md\"",
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
"rebundle": "node tools/cli/bundlers/bundle-web.js rebundle",
"test": "npm run test:schemas && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
"test": "npm run test:schemas && npm run test:refs && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
"test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas",
"test:install": "node test/test-installation-components.js",
"test:refs": "node test/test-file-refs-csv.js",
"test:schemas": "node test/test-agent-schema.js",
"validate:refs": "node tools/validate-file-refs.js",
"validate:schemas": "node tools/validate-agent-schema.js"
@ -68,20 +67,15 @@
"@clack/core": "^1.0.0",
"@clack/prompts": "^1.0.0",
"@kayvan/markdown-tree-parser": "^1.6.1",
"boxen": "^5.1.2",
"chalk": "^4.1.2",
"cli-table3": "^0.6.5",
"commander": "^14.0.0",
"csv-parse": "^6.1.0",
"figlet": "^1.8.0",
"fs-extra": "^11.3.0",
"glob": "^11.0.3",
"ignore": "^7.0.5",
"js-yaml": "^4.1.0",
"ora": "^5.4.1",
"picocolors": "^1.1.1",
"semver": "^7.6.3",
"wrap-ansi": "^7.0.0",
"xml2js": "^0.6.2",
"yaml": "^2.7.0"
},

View File

@ -1,48 +0,0 @@
const fs = require('fs-extra');
const path = require('node:path');
const chalk = require('chalk');
// Directories to create from config
const DIRECTORIES = ['output_folder', 'planning_artifacts', 'implementation_artifacts'];
/**
* BMM Module Installer
* Creates output directories configured in module config
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from module.yaml
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status
*/
async function install(options) {
const { projectRoot, config, logger } = options;
try {
logger.log(chalk.blue('🚀 Installing BMM Module...'));
// Create configured directories
for (const configKey of DIRECTORIES) {
const configValue = config[configKey];
if (!configValue) continue;
const dirPath = configValue.replace('{project-root}/', '');
const fullPath = path.join(projectRoot, dirPath);
if (!(await fs.pathExists(fullPath))) {
const dirName = configKey.replace('_', ' ');
logger.log(chalk.yellow(`Creating ${dirName} directory: ${dirPath}`));
await fs.ensureDir(fullPath);
}
}
logger.log(chalk.green('✓ BMM Module installation complete'));
return true;
} catch (error) {
logger.error(chalk.red(`Error installing BMM module: ${error.message}`));
return false;
}
}
module.exports = { install };

View File

@ -42,3 +42,9 @@ project_knowledge: # Artifacts from research, document-project output, other lon
prompt: "Where should long-term project knowledge be stored? (docs, research, references)"
default: "docs"
result: "{project-root}/{value}"
# Directories to create during installation (declarative, no code execution)
directories:
- "{planning_artifacts}"
- "{implementation_artifacts}"
- "{project_knowledge}"

View File

@ -1,60 +0,0 @@
const chalk = require('chalk');
/**
* Core Module Installer
* Standard module installer function that executes after IDE installations
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from module.yaml
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status
*/
async function install(options) {
const { projectRoot, config, installedIDEs, logger } = options;
try {
logger.log(chalk.blue('🏗️ Installing Core Module...'));
// Core agent configs are created by the main installer's createAgentConfigs method
// No need to create them here - they'll be handled along with all other agents
// Handle IDE-specific configurations if needed
if (installedIDEs && installedIDEs.length > 0) {
logger.log(chalk.cyan(`Configuring Core for IDEs: ${installedIDEs.join(', ')}`));
// Add any IDE-specific Core configurations here
for (const ide of installedIDEs) {
await configureForIDE(ide, projectRoot, config, logger);
}
}
logger.log(chalk.green('✓ Core Module installation complete'));
return true;
} catch (error) {
logger.error(chalk.red(`Error installing Core module: ${error.message}`));
return false;
}
}
/**
* Configure Core module for specific IDE
* @private
*/
async function configureForIDE(ide) {
// Add IDE-specific configurations here
switch (ide) {
case 'claude-code': {
// Claude Code specific Core configurations
break;
}
// Add more IDEs as needed
default: {
// No specific configuration needed
break;
}
}
}
module.exports = { install };

View File

@ -0,0 +1,3 @@
module,phase,name,workflow-file,description
bmm,anytime,Document,,Analyze project
bmm,1-analysis,Brainstorm,,Brainstorm ideas
1 module phase name workflow-file description
2 bmm anytime Document Analyze project
3 bmm 1-analysis Brainstorm Brainstorm ideas

View File

@ -0,0 +1 @@
module,phase,name,workflow-file,description
1 module phase name workflow-file description

View File

@ -0,0 +1,3 @@
name,code,description,agent
brainstorm,BSP,"Generate ideas",analyst
party,PM,"Multi-agent",facilitator
1 name code description agent
2 brainstorm BSP Generate ideas analyst
3 party PM Multi-agent facilitator

View File

@ -0,0 +1,3 @@
module,phase,name,workflow-file,description
bmm,anytime,Template Var,{output_folder}/something.md,Has unresolvable template var
bmm,anytime,Normal Ref,_bmad/core/tasks/help.md,Normal resolvable ref
1 module phase name workflow-file description
2 bmm anytime Template Var {output_folder}/something.md Has unresolvable template var
3 bmm anytime Normal Ref _bmad/core/tasks/help.md Normal resolvable ref

View File

@ -0,0 +1,3 @@
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs,
bmm,anytime,Document Project,DP,,_bmad/bmm/workflows/document-project/workflow.yaml,bmad-bmm-document-project,false,analyst,Create Mode,"Analyze project",project-knowledge,*,
bmm,1-analysis,Brainstorm Project,BP,10,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,data=template.md,"Brainstorming",planning_artifacts,"session",
1 module phase name code sequence workflow-file command required agent options description output-location outputs
2 bmm anytime Document Project DP _bmad/bmm/workflows/document-project/workflow.yaml bmad-bmm-document-project false analyst Create Mode Analyze project project-knowledge *
3 bmm 1-analysis Brainstorm Project BP 10 _bmad/core/workflows/brainstorming/workflow.md bmad-brainstorming false analyst data=template.md Brainstorming planning_artifacts session

View File

@ -0,0 +1,3 @@
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
core,anytime,Brainstorming,BSP,,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,,"Generate ideas",{output_folder}/brainstorming.md,
core,anytime,Party Mode,PM,,_bmad/core/workflows/party-mode/workflow.md,bmad-party-mode,false,facilitator,,"Multi-agent discussion",,
1 module phase name code sequence workflow-file command required agent options description output-location outputs
2 core anytime Brainstorming BSP _bmad/core/workflows/brainstorming/workflow.md bmad-brainstorming false analyst Generate ideas {output_folder}/brainstorming.md
3 core anytime Party Mode PM _bmad/core/workflows/party-mode/workflow.md bmad-party-mode false facilitator Multi-agent discussion

View File

@ -0,0 +1,2 @@
name,workflow-file,description
test,_bmad/core/tasks/help.md,A test entry
1 name workflow-file description
2 test _bmad/core/tasks/help.md A test entry

133
test/test-file-refs-csv.js Normal file
View File

@ -0,0 +1,133 @@
/**
* CSV File Reference Extraction Test Runner
*
* Tests extractCsvRefs() from validate-file-refs.js against fixtures.
* Verifies correct extraction of workflow-file references from CSV files.
*
* Usage: node test/test-file-refs-csv.js
* Exit codes: 0 = all tests pass, 1 = test failures
*/
const fs = require('node:fs');
const path = require('node:path');
const { extractCsvRefs } = require('../tools/validate-file-refs.js');
// ANSI color codes
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
const FIXTURES = path.join(__dirname, 'fixtures/file-refs-csv');
let totalTests = 0;
let passedTests = 0;
const failures = [];
function test(name, fn) {
totalTests++;
try {
fn();
passedTests++;
console.log(` ${colors.green}\u2713${colors.reset} ${name}`);
} catch (error) {
console.log(` ${colors.red}\u2717${colors.reset} ${name} ${colors.red}${error.message}${colors.reset}`);
failures.push({ name, message: error.message });
}
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
function loadFixture(relativePath) {
const fullPath = path.join(FIXTURES, relativePath);
const content = fs.readFileSync(fullPath, 'utf-8');
return { fullPath, content };
}
// --- Valid fixtures ---
console.log(`\n${colors.cyan}CSV File Reference Extraction Tests${colors.reset}\n`);
console.log(`${colors.cyan}Valid fixtures${colors.reset}`);
test('bmm-style.csv: extracts workflow-file refs with trailing commas', () => {
const { fullPath, content } = loadFixture('valid/bmm-style.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 2, `Expected 2 refs, got ${refs.length}`);
assert(refs[0].raw === '_bmad/bmm/workflows/document-project/workflow.yaml', `Wrong raw[0]: ${refs[0].raw}`);
assert(refs[1].raw === '_bmad/core/workflows/brainstorming/workflow.md', `Wrong raw[1]: ${refs[1].raw}`);
assert(refs[0].type === 'project-root', `Wrong type: ${refs[0].type}`);
assert(refs[0].line === 2, `Wrong line for row 0: ${refs[0].line}`);
assert(refs[1].line === 3, `Wrong line for row 1: ${refs[1].line}`);
assert(refs[0].file === fullPath, 'Wrong file path');
});
test('core-style.csv: extracts refs from core module-help format', () => {
const { fullPath, content } = loadFixture('valid/core-style.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 2, `Expected 2 refs, got ${refs.length}`);
assert(refs[0].raw === '_bmad/core/workflows/brainstorming/workflow.md', `Wrong raw[0]: ${refs[0].raw}`);
assert(refs[1].raw === '_bmad/core/workflows/party-mode/workflow.md', `Wrong raw[1]: ${refs[1].raw}`);
});
test('minimal.csv: extracts refs from minimal 3-column CSV', () => {
const { fullPath, content } = loadFixture('valid/minimal.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 1, `Expected 1 ref, got ${refs.length}`);
assert(refs[0].raw === '_bmad/core/tasks/help.md', `Wrong raw: ${refs[0].raw}`);
assert(refs[0].line === 2, `Wrong line: ${refs[0].line}`);
});
// --- Invalid fixtures ---
console.log(`\n${colors.cyan}Invalid fixtures (expect 0 refs)${colors.reset}`);
test('no-workflow-column.csv: returns 0 refs when workflow-file column missing', () => {
const { fullPath, content } = loadFixture('invalid/no-workflow-column.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 0, `Expected 0 refs, got ${refs.length}`);
});
test('empty-data.csv: returns 0 refs when CSV has header only', () => {
const { fullPath, content } = loadFixture('invalid/empty-data.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 0, `Expected 0 refs, got ${refs.length}`);
});
test('all-empty-workflow.csv: returns 0 refs when all workflow-file cells empty', () => {
const { fullPath, content } = loadFixture('invalid/all-empty-workflow.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 0, `Expected 0 refs, got ${refs.length}`);
});
test('unresolvable-vars.csv: filters out template variables, keeps normal refs', () => {
const { fullPath, content } = loadFixture('invalid/unresolvable-vars.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 1, `Expected 1 ref, got ${refs.length}`);
assert(refs[0].raw === '_bmad/core/tasks/help.md', `Wrong raw: ${refs[0].raw}`);
});
// --- Summary ---
console.log(`\n${colors.cyan}${'═'.repeat(55)}${colors.reset}`);
console.log(`${colors.cyan}Test Results:${colors.reset}`);
console.log(` Total: ${totalTests}`);
console.log(` Passed: ${colors.green}${passedTests}${colors.reset}`);
console.log(` Failed: ${passedTests === totalTests ? colors.green : colors.red}${totalTests - passedTests}${colors.reset}`);
console.log(`${colors.cyan}${'═'.repeat(55)}${colors.reset}\n`);
if (failures.length > 0) {
console.log(`${colors.red}FAILED TESTS:${colors.reset}\n`);
for (const failure of failures) {
console.log(`${colors.red}\u2717${colors.reset} ${failure.name}`);
console.log(` ${failure.message}\n`);
}
process.exit(1);
}
console.log(`${colors.green}All tests passed!${colors.reset}\n`);
process.exit(0);

1050
test/test-rehype-plugins.mjs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -52,6 +52,12 @@ const LLM_EXCLUDE_PATTERNS = [
*/
async function main() {
if (process.platform === 'win32') {
console.error('Error: The docs build pipeline does not support Windows.');
console.error('Please build on Linux, macOS, or WSL.');
process.exit(1);
}
console.log();
printBanner('BMAD Documentation Build Pipeline');
console.log();
@ -118,9 +124,6 @@ function buildAstroSite() {
runAstroBuild();
copyArtifactsToSite(artifactsDir, siteDir);
// No longer needed: Inject AI agents banner into every HTML page
// injectAgentBanner(siteDir);
console.log();
console.log(` \u001B[32m✓\u001B[0m Astro build complete`);
@ -152,20 +155,18 @@ function generateLlmsTxt(outputDir) {
'',
'## Quick Start',
'',
`- **[Quick Start](${siteUrl}/docs/modules/bmm/quick-start)** - Get started with BMAD Method`,
`- **[Installation](${siteUrl}/docs/getting-started/installation)** - Installation guide`,
`- **[Getting Started](${siteUrl}/tutorials/getting-started/)** - Tutorial: install and learn how BMad works`,
`- **[Installation](${siteUrl}/how-to/install-bmad/)** - How to install BMad Method`,
'',
'## Core Concepts',
'',
`- **[Scale Adaptive System](${siteUrl}/docs/modules/bmm/scale-adaptive-system)** - Understand BMAD scaling`,
`- **[Quick Flow](${siteUrl}/docs/modules/bmm/bmad-quick-flow)** - Fast development workflow`,
`- **[Party Mode](${siteUrl}/docs/modules/bmm/party-mode)** - Multi-agent collaboration`,
`- **[Quick Flow](${siteUrl}/explanation/quick-flow/)** - Fast development workflow`,
`- **[Party Mode](${siteUrl}/explanation/party-mode/)** - Multi-agent collaboration`,
`- **[Workflow Map](${siteUrl}/reference/workflow-map/)** - Visual overview of phases and workflows`,
'',
'## Modules',
'',
`- **[BMM - Method](${siteUrl}/docs/modules/bmm/quick-start)** - Core methodology module`,
`- **[BMB - Builder](${siteUrl}/docs/modules/bmb/)** - Agent and workflow builder`,
`- **[BMGD - Game Dev](${siteUrl}/docs/modules/bmgd/quick-start)** - Game development module`,
`- **[Official Modules](${siteUrl}/reference/modules/)** - BMM, BMB, BMGD, and more`,
'',
'---',
'',
@ -401,32 +402,6 @@ function formatFileSize(bytes) {
return `${bytes}B`;
}
// =============================================================================
// Post-build Injection
/**
* Recursively collects all files with the given extension under a directory.
*
* @param {string} dir - Root directory to search.
* @param {string} ext - File extension to match (include the leading dot, e.g. ".md").
* @returns {string[]} An array of file paths for files ending with `ext` found under `dir`.
*/
function getAllFilesByExtension(dir, ext) {
const result = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
result.push(...getAllFilesByExtension(fullPath, ext));
} else if (entry.name.endsWith(ext)) {
result.push(fullPath);
}
}
return result;
}
// =============================================================================
// File System Utilities
/**
@ -444,33 +419,6 @@ function cleanBuildDirectory() {
fs.mkdirSync(BUILD_DIR, { recursive: true });
}
/**
* Recursively copies all files and subdirectories from one directory to another, creating the destination if needed.
*
* @param {string} src - Path to the source directory to copy from.
* @param {string} dest - Path to the destination directory to copy to.
* @param {string[]} [exclude=[]] - List of file or directory names (not paths) to skip while copying.
* @returns {boolean} `true` if the source existed and copying proceeded, `false` if the source did not exist.
*/
function copyDirectory(src, dest, exclude = []) {
if (!fs.existsSync(src)) return false;
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
if (exclude.includes(entry.name)) continue;
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirectory(srcPath, destPath, exclude);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
return true;
}
// =============================================================================
// Console Output Formatting
// =============================================================================
@ -496,7 +444,7 @@ function printBanner(title) {
/**
* Verify internal documentation links by running the link-checking script.
*
* Executes the Node script tools/check-doc-links.js from the project root and
* Executes the Node script tools/validate-doc-links.js from the project root and
* exits the process with code 1 if the check fails.
*/

View File

@ -2,6 +2,14 @@ const { program } = require('commander');
const path = require('node:path');
const fs = require('node:fs');
const { execSync } = require('node:child_process');
const prompts = require('./lib/prompts');
// The installer flow uses many sequential @clack/prompts, each adding keypress
// listeners to stdin. Raise the limit to avoid spurious EventEmitter warnings.
if (process.stdin?.setMaxListeners) {
const currentLimit = process.stdin.getMaxListeners();
process.stdin.setMaxListeners(Math.max(currentLimit, 50));
}
// Check for updates - do this asynchronously so it doesn't block startup
const packageJson = require('../../package.json');
@ -27,17 +35,17 @@ async function checkForUpdate() {
}).trim();
if (result && result !== packageJson.version) {
console.warn('');
console.warn(' ╔═══════════════════════════════════════════════════════════════════════════════╗');
console.warn(' ║ UPDATE AVAILABLE ║');
console.warn(' ║ ║');
console.warn(` ║ You are using version ${packageJson.version} but ${result} is available. ║`);
console.warn(' ║ ║');
console.warn(' ║ To update,exir and first run: ║');
console.warn(` ║ npm cache clean --force && npx bmad-method@${tag} install ║`);
console.warn(' ║ ║');
console.warn(' ╚═══════════════════════════════════════════════════════════════════════════════╝');
console.warn('');
const color = await prompts.getColor();
const updateMsg = [
`You are using version ${packageJson.version} but ${result} is available.`,
'',
'To update, exit and first run:',
` npm cache clean --force && npx bmad-method@${tag} install`,
].join('\n');
await prompts.box(updateMsg, 'Update Available', {
rounded: true,
formatBorder: color.yellow,
});
}
} catch {
// Silently fail - network issues or npm not available

View File

@ -1,5 +1,5 @@
const chalk = require('chalk');
const path = require('node:path');
const prompts = require('../lib/prompts');
const { Installer } = require('../installers/lib/core/installer');
const { UI } = require('../lib/ui');
@ -30,14 +30,14 @@ module.exports = {
// Set debug flag as environment variable for all components
if (options.debug) {
process.env.BMAD_DEBUG_MANIFEST = 'true';
console.log(chalk.cyan('Debug mode enabled\n'));
await prompts.log.info('Debug mode enabled');
}
const config = await ui.promptInstall(options);
// Handle cancel
if (config.actionType === 'cancel') {
console.log(chalk.yellow('Installation cancelled.'));
await prompts.log.warn('Installation cancelled.');
process.exit(0);
return;
}
@ -45,13 +45,13 @@ module.exports = {
// Handle quick update separately
if (config.actionType === 'quick-update') {
const result = await installer.quickUpdate(config);
console.log(chalk.green('\n✨ Quick update complete!'));
console.log(chalk.cyan(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`));
await prompts.log.success('Quick update complete!');
await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`);
// Display version-specific end message
const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader();
messageLoader.displayEndMessage();
await messageLoader.displayEndMessage();
process.exit(0);
return;
@ -60,8 +60,8 @@ module.exports = {
// Handle compile agents separately
if (config.actionType === 'compile-agents') {
const result = await installer.compileAgents(config);
console.log(chalk.green('\n✨ Agent recompilation complete!'));
console.log(chalk.cyan(`Recompiled ${result.agentCount} agents with customizations applied`));
await prompts.log.success('Agent recompilation complete!');
await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`);
process.exit(0);
return;
}
@ -80,21 +80,22 @@ module.exports = {
// Display version-specific end message from install-messages.yaml
const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader();
messageLoader.displayEndMessage();
await messageLoader.displayEndMessage();
process.exit(0);
}
} catch (error) {
// Check if error has a complete formatted message
if (error.fullMessage) {
console.error(error.fullMessage);
if (error.stack) {
console.error('\n' + chalk.dim(error.stack));
try {
if (error.fullMessage) {
await prompts.log.error(error.fullMessage);
} else {
await prompts.log.error(`Installation failed: ${error.message}`);
}
} else {
// Generic error handling for all other errors
console.error(chalk.red('Installation failed:'), error.message);
console.error(chalk.dim(error.stack));
if (error.stack) {
await prompts.log.message(error.stack);
}
} catch {
console.error(error.fullMessage || error.message || error);
}
process.exit(1);
}

View File

@ -1,5 +1,5 @@
const chalk = require('chalk');
const path = require('node:path');
const prompts = require('../lib/prompts');
const { Installer } = require('../installers/lib/core/installer');
const { Manifest } = require('../installers/lib/core/manifest');
const { UI } = require('../lib/ui');
@ -21,9 +21,9 @@ module.exports = {
// Check if bmad directory exists
const fs = require('fs-extra');
if (!(await fs.pathExists(bmadDir))) {
console.log(chalk.yellow('No BMAD installation found in the current directory.'));
console.log(chalk.dim(`Expected location: ${bmadDir}`));
console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
await prompts.log.warn('No BMAD installation found in the current directory.');
await prompts.log.message(`Expected location: ${bmadDir}`);
await prompts.log.message('Run "bmad install" to set up a new installation.');
process.exit(0);
return;
}
@ -32,8 +32,8 @@ module.exports = {
const manifestData = await manifest._readRaw(bmadDir);
if (!manifestData) {
console.log(chalk.yellow('No BMAD installation manifest found.'));
console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
await prompts.log.warn('No BMAD installation manifest found.');
await prompts.log.message('Run "bmad install" to set up a new installation.');
process.exit(0);
return;
}
@ -46,7 +46,7 @@ module.exports = {
const availableUpdates = await manifest.checkForUpdates(bmadDir);
// Display status
ui.displayStatus({
await ui.displayStatus({
installation,
modules,
availableUpdates,
@ -55,9 +55,9 @@ module.exports = {
process.exit(0);
} catch (error) {
console.error(chalk.red('Status check failed:'), error.message);
await prompts.log.error(`Status check failed: ${error.message}`);
if (process.env.BMAD_DEBUG) {
console.error(chalk.dim(error.stack));
await prompts.log.message(error.stack);
}
process.exit(1);
}

View File

@ -42,13 +42,12 @@ modules:
type: bmad-org
npmPackage: bmad-method-test-architecture-enterprise
# TODO: Enable once fixes applied:
# whiteport-design-system:
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion
# module-definition: src/module.yaml
# code: WDS
# name: "Whiteport UX Design System"
# description: "UX design framework with Figma integration"
# defaultSelected: false
# type: community
# whiteport-design-system:
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion
# module-definition: src/module.yaml
# code: wds
# name: "Whiteport UX Design System"
# description: "UX design framework with Figma integration"
# defaultSelected: false
# type: community
# npmPackage: bmad-method-wds-expansion

View File

@ -1,7 +1,6 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
const chalk = require('chalk');
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
const { CLIUtils } = require('../../../lib/cli-utils');
const prompts = require('../../../lib/prompts');
@ -189,20 +188,18 @@ class ConfigCollector {
this.allAnswers = {};
}
// Load module's install config schema
// Load module's config schema from module.yaml
// First, try the standard src/modules location
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
// If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
if (!(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source
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');
}
}
@ -212,8 +209,6 @@ class ConfigCollector {
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else {
// Check if this is a custom module with custom.yaml
const { ModuleManager } = require('../modules/manager');
@ -222,9 +217,8 @@ class ConfigCollector {
if (moduleSourcePath) {
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
const moduleInstallerCustomPath = path.join(moduleSourcePath, '_module-installer', 'custom.yaml');
if ((await fs.pathExists(rootCustomConfigPath)) || (await fs.pathExists(moduleInstallerCustomPath))) {
if (await fs.pathExists(rootCustomConfigPath)) {
isCustomModule = true;
// For custom modules, we don't have an install-config schema, so just use existing values
// The custom.yaml values will be loaded and merged during installation
@ -260,15 +254,9 @@ class ConfigCollector {
// If module has no config keys at all, handle it specially
if (hasNoConfig && moduleConfig.subheader) {
// Add blank line for better readability (matches other modules)
console.log();
const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
// Display the module name in color first (matches other modules)
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// Show the subheader since there's no configuration to ask about
console.log(chalk.dim(`${moduleConfig.subheader}`));
await prompts.log.step(moduleDisplayName);
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
return false; // No new fields
}
@ -322,7 +310,7 @@ class ConfigCollector {
}
// Show "no config" message for modules with no new questions (that have config keys)
console.log(chalk.dim(` ${moduleName.toUpperCase()} module already up to date`));
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module already up to date`);
return false; // No new fields
}
@ -350,15 +338,15 @@ class ConfigCollector {
if (questions.length > 0) {
// Only show header if we actually have questions
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
console.log(); // Line break before questions
await CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
await prompts.log.message('');
const promptedAnswers = await prompts.prompt(questions);
// Merge prompted answers with static answers
Object.assign(allAnswers, promptedAnswers);
} else if (newStaticKeys.length > 0) {
// Only static fields, no questions - show no config message
console.log(chalk.dim(` ${moduleName.toUpperCase()} module configuration updated`));
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configuration updated`);
}
// Store all answers for cross-referencing
@ -507,28 +495,24 @@ class ConfigCollector {
}
// Load module's config
// First, check if we have a custom module path for this module
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 {
// Try the standard src/modules location
installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
}
// If not found in src/modules or custom paths, search the project
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
if (!(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source
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');
}
}
@ -536,8 +520,6 @@ class ConfigCollector {
let configPath = null;
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else {
// No config for this module
return;
@ -588,7 +570,7 @@ class ConfigCollector {
// Skip prompts mode: use all defaults without asking
if (this.skipPrompts) {
console.log(chalk.cyan('Using default configuration for'), chalk.magenta(moduleDisplayName));
await prompts.log.info(`Using default configuration for ${moduleDisplayName}`);
// Use defaults for all questions
for (const question of questions) {
const hasDefault = question.default !== undefined && question.default !== null && question.default !== '';
@ -597,12 +579,10 @@ class ConfigCollector {
}
}
} else {
console.log();
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
await prompts.log.step(moduleDisplayName);
let customize = true;
if (moduleName === 'core') {
// Core module: no confirm prompt, so add spacing manually to match visual style
console.log(chalk.gray('│'));
// Core module: no confirm prompt, continues directly
} else {
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
const customizeAnswer = await prompts.prompt([
@ -621,7 +601,7 @@ class ConfigCollector {
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
if (questionsWithoutDefaults.length > 0) {
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
await prompts.log.message(` Asking required questions for ${moduleName.toUpperCase()}...`);
const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
Object.assign(allAnswers, promptedAnswers);
}
@ -747,32 +727,15 @@ class ConfigCollector {
const hasNoConfig = actualConfigKeys.length === 0;
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
// Module explicitly has no configuration - show with special styling
// Add blank line for better readability (matches other modules)
console.log();
// Display the module name in color first (matches other modules)
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// Ask user if they want to accept defaults or customize on the next line
const { customize } = await prompts.prompt([
{
type: 'confirm',
name: 'customize',
message: 'Accept Defaults (no to customize)?',
default: true,
},
]);
// Show the subheader if available, otherwise show a default message
await prompts.log.step(moduleDisplayName);
if (moduleConfig.subheader) {
console.log(chalk.dim(` ${moduleConfig.subheader}`));
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
} else {
console.log(chalk.dim(` ✓ No custom configuration required`));
await prompts.log.message(` \u2713 No custom configuration required`);
}
} else {
// Module has config but just no questions to ask
console.log(chalk.dim(` ${moduleName.toUpperCase()} module configured`));
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
}
}
@ -981,14 +944,15 @@ class ConfigCollector {
}
// Add current value indicator for existing configs
const color = await prompts.getColor();
if (existingValue !== null && existingValue !== undefined) {
if (typeof existingValue === 'boolean') {
message += chalk.dim(` (current: ${existingValue ? 'true' : 'false'})`);
message += color.dim(` (current: ${existingValue ? 'true' : 'false'})`);
} else if (Array.isArray(existingValue)) {
message += chalk.dim(` (current: ${existingValue.join(', ')})`);
message += color.dim(` (current: ${existingValue.join(', ')})`);
} else if (questionType !== 'list') {
// Show the cleaned value (without {project-root}/) for display
message += chalk.dim(` (current: ${existingValue})`);
message += color.dim(` (current: ${existingValue})`);
}
} else if (item.example && questionType === 'input') {
// Show example for input fields
@ -998,7 +962,7 @@ class ConfigCollector {
exampleText = this.replacePlaceholders(exampleText, moduleName, moduleConfig);
exampleText = exampleText.replace('{project-root}/', '');
}
message += chalk.dim(` (e.g., ${exampleText})`);
message += color.dim(` (e.g., ${exampleText})`);
}
// Build the question object

View File

@ -1,8 +1,8 @@
const fs = require('fs-extra');
const path = require('node:path');
const glob = require('glob');
const chalk = require('chalk');
const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
/**
* Dependency Resolver for BMAD modules
@ -24,7 +24,7 @@ class DependencyResolver {
*/
async resolve(bmadDir, selectedModules = [], options = {}) {
if (options.verbose) {
console.log(chalk.cyan('Resolving module dependencies...'));
await prompts.log.info('Resolving module dependencies...');
}
// Always include core as base
@ -50,7 +50,7 @@ class DependencyResolver {
// Report results (only in verbose mode)
if (options.verbose) {
this.reportResults(organizedFiles, selectedModules);
await this.reportResults(organizedFiles, selectedModules);
}
return {
@ -90,8 +90,12 @@ class DependencyResolver {
}
}
if (!moduleDir) {
continue;
}
if (!(await fs.pathExists(moduleDir))) {
console.warn(chalk.yellow(`Module directory not found: ${moduleDir}`));
await prompts.log.warn('Module directory not found: ' + moduleDir);
continue;
}
@ -179,7 +183,7 @@ class DependencyResolver {
}
}
} catch (error) {
console.warn(chalk.yellow(`Failed to parse frontmatter in ${file.name}: ${error.message}`));
await prompts.log.warn('Failed to parse frontmatter in ' + file.name + ': ' + error.message);
}
}
@ -658,8 +662,8 @@ class DependencyResolver {
/**
* Report resolution results
*/
reportResults(organized, selectedModules) {
console.log(chalk.green('\n✓ Dependency resolution complete'));
async reportResults(organized, selectedModules) {
await prompts.log.success('Dependency resolution complete');
for (const [module, files] of Object.entries(organized)) {
const isSelected = selectedModules.includes(module) || module === 'core';
@ -667,31 +671,31 @@ class DependencyResolver {
files.agents.length + files.tasks.length + files.tools.length + files.templates.length + files.data.length + files.other.length;
if (totalFiles > 0) {
console.log(chalk.cyan(`\n ${module.toUpperCase()} module:`));
console.log(chalk.dim(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`));
await prompts.log.info(` ${module.toUpperCase()} module:`);
await prompts.log.message(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`);
if (files.agents.length > 0) {
console.log(chalk.dim(` Agents: ${files.agents.length}`));
await prompts.log.message(` Agents: ${files.agents.length}`);
}
if (files.tasks.length > 0) {
console.log(chalk.dim(` Tasks: ${files.tasks.length}`));
await prompts.log.message(` Tasks: ${files.tasks.length}`);
}
if (files.templates.length > 0) {
console.log(chalk.dim(` Templates: ${files.templates.length}`));
await prompts.log.message(` Templates: ${files.templates.length}`);
}
if (files.data.length > 0) {
console.log(chalk.dim(` Data files: ${files.data.length}`));
await prompts.log.message(` Data files: ${files.data.length}`);
}
if (files.other.length > 0) {
console.log(chalk.dim(` Other files: ${files.other.length}`));
await prompts.log.message(` Other files: ${files.other.length}`);
}
}
}
if (this.missingDependencies.size > 0) {
console.log(chalk.yellow('\n ⚠ Missing dependencies:'));
await prompts.log.warn('Missing dependencies:');
for (const missing of this.missingDependencies) {
console.log(chalk.yellow(` - ${missing}`));
await prompts.log.warn(` - ${missing}`);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
const { FileOps } = require('../../../lib/file-ops');
const { XmlHandler } = require('../../../lib/xml-handler');
@ -55,7 +55,7 @@ class CustomHandler {
// Found a custom.yaml file
customPaths.push(fullPath);
} else if (
entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory)
entry.name === 'module.yaml' && // Check if this is a custom module (in root directory)
// Skip if it's in src/modules (those are standard modules)
!fullPath.includes(path.join('src', 'modules'))
) {
@ -88,7 +88,7 @@ class CustomHandler {
try {
config = yaml.parse(configContent);
} catch (parseError) {
console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
await prompts.log.warn('YAML parse error in ' + configPath + ': ' + parseError.message);
return null;
}
@ -111,7 +111,7 @@ class CustomHandler {
isInstallConfig: isInstallConfig, // Track which type this is
};
} catch (error) {
console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message));
await prompts.log.warn('Failed to read ' + configPath + ': ' + error.message);
return null;
}
}
@ -268,14 +268,13 @@ class CustomHandler {
}
results.filesCopied++;
if (entry.name.endsWith('.md')) {
results.workflowsInstalled++;
}
if (fileTrackingCallback) {
fileTrackingCallback(targetPath);
}
}
if (entry.name.endsWith('.md')) {
results.workflowsInstalled++;
}
} catch (error) {
results.errors.push(`Failed to copy ${entry.name}: ${error.message}`);
}
@ -322,7 +321,7 @@ class CustomHandler {
await fs.writeFile(customizePath, templateContent, 'utf8');
// Only show customize creation in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
await prompts.log.message(' Created customize: custom-' + agentName + '.customize.yaml');
}
}
}
@ -346,14 +345,10 @@ class CustomHandler {
// Only show compilation details in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
);
await prompts.log.message(' Compiled agent: ' + agentName + ' -> ' + path.relative(targetAgentsPath, targetMdPath));
}
} catch (error) {
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
await prompts.log.warn(' Failed to compile agent ' + agentName + ': ' + error.message);
results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`);
}
}

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const { XmlHandler } = require('../../../lib/xml-handler');
const prompts = require('../../../lib/prompts');
const { getSourcePath } = require('../../../lib/project-root');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
@ -53,7 +53,7 @@ class BaseIdeSetup {
* Cleanup IDE configuration
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir) {
async cleanup(projectDir, options = {}) {
// Default implementation - can be overridden
if (this.configDir) {
const configPath = path.join(projectDir, this.configDir);
@ -61,7 +61,7 @@ class BaseIdeSetup {
const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME);
if (await fs.pathExists(bmadRulesPath)) {
await fs.remove(bmadRulesPath);
console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
if (!options.silent) await prompts.log.message(`Removed ${this.name} BMAD configuration`);
}
}
}

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const { BaseIdeSetup } = require('./_base-ide');
const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
@ -34,10 +34,10 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
* @returns {Promise<Object>} Setup result
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
// Clean up any old BMAD installation first
await this.cleanup(projectDir);
await this.cleanup(projectDir, options);
if (!this.installerConfig) {
return { success: false, reason: 'no-config' };
@ -102,7 +102,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
results.tools = taskToolResult.tools || 0;
}
this.printSummary(results, target_dir);
await this.printSummary(results, target_dir, options);
return { success: true, results };
}
@ -439,32 +439,28 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @param {Object} results - Installation results
* @param {string} targetDir - Target directory (relative)
*/
printSummary(results, targetDir) {
console.log(chalk.green(`\n${this.name} configured:`));
if (results.agents > 0) {
console.log(chalk.dim(` - ${results.agents} agents installed`));
}
if (results.workflows > 0) {
console.log(chalk.dim(` - ${results.workflows} workflow commands generated`));
}
if (results.tasks > 0 || results.tools > 0) {
console.log(chalk.dim(` - ${results.tasks + results.tools} task/tool commands generated`));
}
console.log(chalk.dim(` - Destination: ${targetDir}`));
async printSummary(results, targetDir, options = {}) {
if (options.silent) return;
const parts = [];
if (results.agents > 0) parts.push(`${results.agents} agents`);
if (results.workflows > 0) parts.push(`${results.workflows} workflows`);
if (results.tasks > 0) parts.push(`${results.tasks} tasks`);
if (results.tools > 0) parts.push(`${results.tools} tools`);
await prompts.log.success(`${this.name} configured: ${parts.join(', ')}${targetDir}`);
}
/**
* Cleanup IDE configuration
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir) {
async cleanup(projectDir, options = {}) {
// Clean all target directories
if (this.installerConfig?.targets) {
for (const target of this.installerConfig.targets) {
await this.cleanupTarget(projectDir, target.target_dir);
await this.cleanupTarget(projectDir, target.target_dir, options);
}
} else if (this.installerConfig?.target_dir) {
await this.cleanupTarget(projectDir, this.installerConfig.target_dir);
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options);
}
}
@ -473,7 +469,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @param {string} projectDir - Project directory
* @param {string} targetDir - Target directory to clean
*/
async cleanupTarget(projectDir, targetDir) {
async cleanupTarget(projectDir, targetDir, options = {}) {
const targetPath = path.join(projectDir, targetDir);
if (!(await fs.pathExists(targetPath))) {
@ -496,25 +492,22 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
let removedCount = 0;
for (const entry of entries) {
// Skip non-strings or undefined entries
if (!entry || typeof entry !== 'string') {
continue;
}
if (entry.startsWith('bmad')) {
const entryPath = path.join(targetPath, entry);
const stat = await fs.stat(entryPath);
if (stat.isFile()) {
await fs.remove(entryPath);
removedCount++;
} else if (stat.isDirectory()) {
try {
await fs.remove(entryPath);
removedCount++;
} catch {
// Skip entries that can't be removed (broken symlinks, permission errors)
}
}
}
if (removedCount > 0) {
console.log(chalk.dim(` Cleaned ${removedCount} BMAD files from ${targetDir}`));
if (removedCount > 0 && !options.silent) {
await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`);
}
}
}

View File

@ -1,7 +1,6 @@
const path = require('node:path');
const fs = require('fs-extra');
const os = require('node:os');
const chalk = require('chalk');
const { BaseIdeSetup } = require('./_base-ide');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
@ -43,12 +42,11 @@ class CodexSetup extends BaseIdeSetup {
default: 'global',
});
// Display detailed instructions for the chosen option
console.log('');
// Show brief confirmation hint (detailed instructions available via verbose)
if (installLocation === 'project') {
console.log(this.getProjectSpecificInstructions());
await prompts.log.info('Prompts installed to: <project>/.codex/prompts (requires CODEX_HOME)');
} else {
console.log(this.getGlobalInstructions());
await prompts.log.info('Prompts installed to: ~/.codex/prompts');
}
// Confirm the choice
@ -58,7 +56,7 @@ class CodexSetup extends BaseIdeSetup {
});
if (!confirmed) {
console.log(chalk.yellow("\n Let's choose a different installation option.\n"));
await prompts.log.warn("Let's choose a different installation option.");
}
}
@ -72,7 +70,7 @@ class CodexSetup extends BaseIdeSetup {
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
// Always use CLI mode
const mode = 'cli';
@ -84,7 +82,7 @@ class CodexSetup extends BaseIdeSetup {
const destDir = this.getCodexPromptDir(projectDir, installLocation);
await fs.ensureDir(destDir);
await this.clearOldBmadFiles(destDir);
await this.clearOldBmadFiles(destDir, options);
// Collect artifacts and write using underscore format
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
@ -124,16 +122,11 @@ class CodexSetup extends BaseIdeSetup {
const written = agentCount + workflowCount + tasksWritten;
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - Mode: CLI`));
console.log(chalk.dim(` - ${counts.agents} agents exported`));
console.log(chalk.dim(` - ${counts.tasks} tasks exported`));
console.log(chalk.dim(` - ${counts.workflows} workflow commands exported`));
if (counts.workflowLaunchers > 0) {
console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers exported`));
if (!options.silent) {
await prompts.log.success(
`${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} files → ${destDir}`,
);
}
console.log(chalk.dim(` - ${written} Codex prompt files written`));
console.log(chalk.dim(` - Destination: ${destDir}`));
return {
success: true,
@ -262,7 +255,7 @@ class CodexSetup extends BaseIdeSetup {
return written;
}
async clearOldBmadFiles(destDir) {
async clearOldBmadFiles(destDir, options = {}) {
if (!(await fs.pathExists(destDir))) {
return;
}
@ -272,7 +265,7 @@ class CodexSetup extends BaseIdeSetup {
entries = await fs.readdir(destDir);
} catch (error) {
// Directory exists but can't be read - skip cleanup
console.warn(chalk.yellow(`Warning: Could not read directory ${destDir}: ${error.message}`));
if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`);
return;
}
@ -291,15 +284,11 @@ class CodexSetup extends BaseIdeSetup {
const entryPath = path.join(destDir, entry);
try {
const stat = await fs.stat(entryPath);
if (stat.isFile()) {
await fs.remove(entryPath);
} else if (stat.isDirectory()) {
await fs.remove(entryPath);
}
await fs.remove(entryPath);
} catch (error) {
// Skip files that can't be processed
console.warn(chalk.dim(` Skipping ${entry}: ${error.message}`));
if (!options.silent) {
await prompts.log.message(` Skipping ${entry}: ${error.message}`);
}
}
}
}
@ -315,22 +304,16 @@ class CodexSetup extends BaseIdeSetup {
*/
getGlobalInstructions(destDir) {
const lines = [
'IMPORTANT: Codex Configuration',
'',
chalk.bold.cyan('═'.repeat(70)),
chalk.bold.yellow(' IMPORTANT: Codex Configuration'),
chalk.bold.cyan('═'.repeat(70)),
'/prompts installed globally to your HOME DIRECTORY.',
'',
chalk.white(' /prompts installed globally to your HOME DIRECTORY.'),
'',
chalk.yellow(' ⚠️ These prompts reference a specific _bmad path'),
chalk.dim(" To use with other projects, you'd need to copy the _bmad dir"),
'',
chalk.green(' ✓ You can now use /commands in Codex CLI'),
chalk.dim(' Example: /bmad_bmm_pm'),
chalk.dim(' Type / to see all available commands'),
'',
chalk.bold.cyan('═'.repeat(70)),
'These prompts reference a specific _bmad path.',
"To use with other projects, you'd need to copy the _bmad dir.",
'',
'You can now use /commands in Codex CLI',
' Example: /bmad_bmm_pm',
' Type / to see all available commands',
];
return lines.join('\n');
}
@ -345,43 +328,34 @@ class CodexSetup extends BaseIdeSetup {
const isWindows = os.platform() === 'win32';
const commonLines = [
'Project-Specific Codex Configuration',
'',
chalk.bold.cyan('═'.repeat(70)),
chalk.bold.yellow(' Project-Specific Codex Configuration'),
chalk.bold.cyan('═'.repeat(70)),
`Prompts will be installed to: ${destDir || '<project>/.codex/prompts'}`,
'',
chalk.white(' Prompts will be installed to: ') + chalk.cyan(destDir || '<project>/.codex/prompts'),
'',
chalk.bold.yellow(' ⚠️ REQUIRED: You must set CODEX_HOME to use these prompts'),
'REQUIRED: You must set CODEX_HOME to use these prompts',
'',
];
const windowsLines = [
chalk.bold(' Create a codex.cmd file in your project root:'),
'Create a codex.cmd file in your project root:',
'',
chalk.green(' @echo off'),
chalk.green(' set CODEX_HOME=%~dp0.codex'),
chalk.green(' codex %*'),
' @echo off',
' set CODEX_HOME=%~dp0.codex',
' codex %*',
'',
chalk.dim(String.raw` Then run: .\codex instead of codex`),
chalk.dim(' (The %~dp0 gets the directory of the .cmd file)'),
String.raw`Then run: .\codex instead of codex`,
'(The %~dp0 gets the directory of the .cmd file)',
];
const unixLines = [
chalk.bold(' Add this alias to your ~/.bashrc or ~/.zshrc:'),
'Add this alias to your ~/.bashrc or ~/.zshrc:',
'',
chalk.green(' alias codex=\'CODEX_HOME="$PWD/.codex" codex\''),
'',
chalk.dim(' After adding, run: source ~/.bashrc (or source ~/.zshrc)'),
chalk.dim(' (The $PWD uses your current working directory)'),
];
const closingLines = [
'',
chalk.dim(' This tells Codex CLI to use prompts from this project instead of ~/.codex'),
'',
chalk.bold.cyan('═'.repeat(70)),
' alias codex=\'CODEX_HOME="$PWD/.codex" codex\'',
'',
'After adding, run: source ~/.bashrc (or source ~/.zshrc)',
'(The $PWD uses your current working directory)',
];
const closingLines = ['', 'This tells Codex CLI to use prompts from this project instead of ~/.codex'];
const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines];

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
@ -23,10 +23,10 @@ class KiloSetup extends BaseIdeSetup {
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
// Clean up any old BMAD installation first
await this.cleanup(projectDir);
await this.cleanup(projectDir, options);
// Load existing config (may contain non-BMAD modes and other settings)
const kiloModesPath = path.join(projectDir, this.configFile);
@ -38,7 +38,7 @@ class KiloSetup extends BaseIdeSetup {
config = yaml.parse(existingContent) || {};
} catch {
// If parsing fails, start fresh but warn user
console.log(chalk.yellow('Warning: Could not parse existing .kilocodemodes, starting fresh'));
await prompts.log.warn('Warning: Could not parse existing .kilocodemodes, starting fresh');
config = {};
}
}
@ -88,14 +88,11 @@ class KiloSetup extends BaseIdeSetup {
const taskCount = taskToolCounts.tasks || 0;
const toolCount = taskToolCounts.tools || 0;
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${addedCount} modes added`));
console.log(chalk.dim(` - ${workflowCount} workflows exported`));
console.log(chalk.dim(` - ${taskCount} tasks exported`));
console.log(chalk.dim(` - ${toolCount} tools exported`));
console.log(chalk.dim(` - Configuration file: ${this.configFile}`));
console.log(chalk.dim(` - Workflows directory: .kilocode/workflows/`));
console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode'));
if (!options.silent) {
await prompts.log.success(
`${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`,
);
}
return {
success: true,
@ -174,7 +171,7 @@ class KiloSetup extends BaseIdeSetup {
/**
* Cleanup KiloCode configuration
*/
async cleanup(projectDir) {
async cleanup(projectDir, options = {}) {
const fs = require('fs-extra');
const kiloModesPath = path.join(projectDir, this.configFile);
@ -192,12 +189,12 @@ class KiloSetup extends BaseIdeSetup {
if (removedCount > 0) {
await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 }));
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`));
if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .kilocodemodes`);
}
}
} catch {
// If parsing fails, leave file as-is
console.log(chalk.yellow('Warning: Could not parse .kilocodemodes for cleanup'));
if (!options.silent) await prompts.log.warn('Warning: Could not parse .kilocodemodes for cleanup');
}
}

View File

@ -1,326 +0,0 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const fs = require('fs-extra');
const yaml = require('yaml');
/**
* Kiro CLI setup handler for BMad Method
*/
class KiroCliSetup extends BaseIdeSetup {
constructor() {
super('kiro-cli', 'Kiro CLI', false);
this.configDir = '.kiro';
this.agentsDir = 'agents';
}
/**
* Cleanup old BMAD installation before reinstalling
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir) {
const bmadAgentsDir = path.join(projectDir, this.configDir, this.agentsDir);
if (await fs.pathExists(bmadAgentsDir)) {
// Remove existing BMad agents
const files = await fs.readdir(bmadAgentsDir);
for (const file of files) {
if (file.startsWith('bmad')) {
await fs.remove(path.join(bmadAgentsDir, file));
}
}
console.log(chalk.dim(` Cleaned old BMAD agents from ${this.name}`));
}
}
/**
* Setup Kiro CLI configuration with BMad agents
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
await this.cleanup(projectDir);
const kiroDir = path.join(projectDir, this.configDir);
const agentsDir = path.join(kiroDir, this.agentsDir);
await this.ensureDir(agentsDir);
// Create BMad agents from source YAML files
await this.createBmadAgentsFromSource(agentsDir, projectDir);
console.log(chalk.green(`${this.name} configured with BMad agents`));
}
/**
* Create BMad agent definitions from source YAML files
* @param {string} agentsDir - Agents directory
* @param {string} projectDir - Project directory
*/
async createBmadAgentsFromSource(agentsDir, projectDir) {
const sourceDir = path.join(__dirname, '../../../../../src/modules');
// Find all agent YAML files
const agentFiles = await this.findAgentFiles(sourceDir);
for (const agentFile of agentFiles) {
try {
await this.processAgentFile(agentFile, agentsDir, projectDir);
} catch (error) {
console.warn(chalk.yellow(`⚠️ Failed to process ${agentFile}: ${error.message}`));
}
}
}
/**
* Find all agent YAML files in modules and core
* @param {string} sourceDir - Source modules directory
* @returns {Array} Array of agent file paths
*/
async findAgentFiles(sourceDir) {
const agentFiles = [];
// Check core agents
const coreAgentsDir = path.join(__dirname, '../../../../../src/core/agents');
if (await fs.pathExists(coreAgentsDir)) {
const files = await fs.readdir(coreAgentsDir);
for (const file of files) {
if (file.endsWith('.agent.yaml')) {
agentFiles.push(path.join(coreAgentsDir, file));
}
}
}
// Check module agents
if (!(await fs.pathExists(sourceDir))) {
return agentFiles;
}
const modules = await fs.readdir(sourceDir);
for (const module of modules) {
const moduleAgentsDir = path.join(sourceDir, module, 'agents');
if (await fs.pathExists(moduleAgentsDir)) {
const files = await fs.readdir(moduleAgentsDir);
for (const file of files) {
if (file.endsWith('.agent.yaml')) {
agentFiles.push(path.join(moduleAgentsDir, file));
}
}
}
}
return agentFiles;
}
/**
* Validate BMad Core compliance
* @param {Object} agentData - Agent YAML data
* @returns {boolean} True if compliant
*/
validateBmadCompliance(agentData) {
const requiredFields = ['agent.metadata.id', 'agent.persona.role', 'agent.persona.principles'];
for (const field of requiredFields) {
const keys = field.split('.');
let current = agentData;
for (const key of keys) {
if (!current || !current[key]) {
return false;
}
current = current[key];
}
}
return true;
}
/**
* Process individual agent YAML file
* @param {string} agentFile - Path to agent YAML file
* @param {string} agentsDir - Target agents directory
* @param {string} projectDir - Project directory
*/
async processAgentFile(agentFile, agentsDir, projectDir) {
const yamlContent = await fs.readFile(agentFile, 'utf8');
const agentData = yaml.parse(yamlContent);
if (!this.validateBmadCompliance(agentData)) {
return;
}
// Extract module from file path
const normalizedPath = path.normalize(agentFile);
const pathParts = normalizedPath.split(path.sep);
const basename = path.basename(agentFile, '.agent.yaml');
// Find the module name from path
let moduleName = 'unknown';
if (pathParts.includes('src')) {
const srcIndex = pathParts.indexOf('src');
if (srcIndex + 3 < pathParts.length) {
const folderAfterSrc = pathParts[srcIndex + 1];
if (folderAfterSrc === 'core') {
moduleName = 'core';
} else if (folderAfterSrc === 'bmm') {
moduleName = 'bmm';
}
}
}
// Extract the agent name from the ID path in YAML if available
let agentBaseName = basename;
if (agentData.agent && agentData.agent.metadata && agentData.agent.metadata.id) {
const idPath = agentData.agent.metadata.id;
agentBaseName = path.basename(idPath, '.md');
}
const agentName = `bmad-${moduleName}-${agentBaseName}`;
const sanitizedAgentName = this.sanitizeAgentName(agentName);
// Create JSON definition
await this.createAgentDefinitionFromYaml(agentsDir, sanitizedAgentName, agentData);
// Create prompt file
await this.createAgentPromptFromYaml(agentsDir, sanitizedAgentName, agentData, projectDir);
}
/**
* Sanitize agent name for file naming
* @param {string} name - Agent name
* @returns {string} Sanitized name
*/
sanitizeAgentName(name) {
return name
.toLowerCase()
.replaceAll(/\s+/g, '-')
.replaceAll(/[^a-z0-9-]/g, '');
}
/**
* Create agent JSON definition from YAML data
* @param {string} agentsDir - Agents directory
* @param {string} agentName - Agent name (role-based)
* @param {Object} agentData - Agent YAML data
*/
async createAgentDefinitionFromYaml(agentsDir, agentName, agentData) {
const personName = agentData.agent.metadata.name;
const role = agentData.agent.persona.role;
const agentConfig = {
name: agentName,
description: `${personName} - ${role}`,
prompt: `file://./${agentName}-prompt.md`,
tools: ['*'],
mcpServers: {},
useLegacyMcpJson: true,
resources: [],
};
const agentPath = path.join(agentsDir, `${agentName}.json`);
await fs.writeJson(agentPath, agentConfig, { spaces: 2 });
}
/**
* Create agent prompt from YAML data
* @param {string} agentsDir - Agents directory
* @param {string} agentName - Agent name (role-based)
* @param {Object} agentData - Agent YAML data
* @param {string} projectDir - Project directory
*/
async createAgentPromptFromYaml(agentsDir, agentName, agentData, projectDir) {
const promptPath = path.join(agentsDir, `${agentName}-prompt.md`);
// Generate prompt from YAML data
const prompt = this.generatePromptFromYaml(agentData);
await fs.writeFile(promptPath, prompt);
}
/**
* Generate prompt content from YAML data
* @param {Object} agentData - Agent YAML data
* @returns {string} Generated prompt
*/
generatePromptFromYaml(agentData) {
const agent = agentData.agent;
const name = agent.metadata.name;
const icon = agent.metadata.icon || '🤖';
const role = agent.persona.role;
const identity = agent.persona.identity;
const style = agent.persona.communication_style;
const principles = agent.persona.principles;
let prompt = `# ${name} ${icon}\n\n`;
prompt += `## Role\n${role}\n\n`;
if (identity) {
prompt += `## Identity\n${identity}\n\n`;
}
if (style) {
prompt += `## Communication Style\n${style}\n\n`;
}
if (principles) {
prompt += `## Principles\n`;
if (typeof principles === 'string') {
// Handle multi-line string principles
prompt += principles + '\n\n';
} else if (Array.isArray(principles)) {
// Handle array principles
for (const principle of principles) {
prompt += `- ${principle}\n`;
}
prompt += '\n';
}
}
// Add menu items if available
if (agent.menu && agent.menu.length > 0) {
prompt += `## Available Workflows\n`;
for (let i = 0; i < agent.menu.length; i++) {
const item = agent.menu[i];
prompt += `${i + 1}. **${item.trigger}**: ${item.description}\n`;
}
prompt += '\n';
}
prompt += `## Instructions\nYou are ${name}, part of the BMad Method. Follow your role and principles while assisting users with their development needs.\n`;
return prompt;
}
/**
* Check if Kiro CLI is available
* @returns {Promise<boolean>} True if available
*/
async isAvailable() {
try {
const { execSync } = require('node:child_process');
execSync('kiro-cli --version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
/**
* Get installation instructions
* @returns {string} Installation instructions
*/
getInstallInstructions() {
return `Install Kiro CLI:
curl -fsSL https://github.com/aws/kiro-cli/releases/latest/download/install.sh | bash
Or visit: https://github.com/aws/kiro-cli`;
}
}
module.exports = { KiroCliSetup };

View File

@ -1,14 +1,14 @@
const fs = require('fs-extra');
const path = require('node:path');
const chalk = require('chalk');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
const prompts = require('../../../lib/prompts');
/**
* IDE Manager - handles IDE-specific setup
* Dynamically discovers and loads IDE handlers
*
* Loading strategy:
* 1. Custom installer files (codex.js, kilo.js, kiro-cli.js) - for platforms with unique installation logic
* 1. Custom installer files (codex.js, kilo.js) - for platforms with unique installation logic
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
*/
class IdeManager {
@ -44,12 +44,12 @@ class IdeManager {
/**
* Dynamically load all IDE handlers
* 1. Load custom installer files first (codex.js, kilo.js, kiro-cli.js)
* 1. Load custom installer files first (codex.js, kilo.js)
* 2. Load config-driven handlers from platform-codes.yaml
*/
async loadHandlers() {
// Load custom installer files
this.loadCustomInstallerFiles();
await this.loadCustomInstallerFiles();
// Load config-driven handlers from platform-codes.yaml
await this.loadConfigDrivenHandlers();
@ -59,9 +59,9 @@ class IdeManager {
* Load custom installer files (unique installation logic)
* These files have special installation patterns that don't fit the config-driven model
*/
loadCustomInstallerFiles() {
async loadCustomInstallerFiles() {
const ideDir = __dirname;
const customFiles = ['codex.js', 'kilo.js', 'kiro-cli.js'];
const customFiles = ['codex.js', 'kilo.js'];
for (const file of customFiles) {
const filePath = path.join(ideDir, file);
@ -81,7 +81,7 @@ class IdeManager {
}
}
} catch (error) {
console.log(chalk.yellow(` Warning: Could not load ${file}: ${error.message}`));
await prompts.log.warn(`Warning: Could not load ${file}: ${error.message}`);
}
}
}
@ -171,17 +171,45 @@ class IdeManager {
const handler = this.handlers.get(ideName.toLowerCase());
if (!handler) {
console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported`));
console.log(chalk.dim('Supported IDEs:', [...this.handlers.keys()].join(', ')));
return { success: false, reason: 'unsupported' };
await prompts.log.warn(`IDE '${ideName}' is not yet supported`);
await prompts.log.message(`Supported IDEs: ${[...this.handlers.keys()].join(', ')}`);
return { success: false, ide: ideName, error: 'unsupported IDE' };
}
try {
await handler.setup(projectDir, bmadDir, options);
return { success: true, ide: ideName };
const handlerResult = await handler.setup(projectDir, bmadDir, options);
// Build detail string from handler-returned data
let detail = '';
if (handlerResult && handlerResult.results) {
// Config-driven handlers return { success, results: { agents, workflows, tasks, tools } }
const r = handlerResult.results;
const parts = [];
if (r.agents > 0) parts.push(`${r.agents} agents`);
if (r.workflows > 0) parts.push(`${r.workflows} workflows`);
if (r.tasks > 0) parts.push(`${r.tasks} tasks`);
if (r.tools > 0) parts.push(`${r.tools} tools`);
detail = parts.join(', ');
} else if (handlerResult && handlerResult.counts) {
// Codex handler returns { success, counts: { agents, workflows, tasks }, written }
const c = handlerResult.counts;
const parts = [];
if (c.agents > 0) parts.push(`${c.agents} agents`);
if (c.workflows > 0) parts.push(`${c.workflows} workflows`);
if (c.tasks > 0) parts.push(`${c.tasks} tasks`);
detail = parts.join(', ');
} else if (handlerResult && handlerResult.modes !== undefined) {
// Kilo handler returns { success, modes, workflows, tasks, tools }
const parts = [];
if (handlerResult.modes > 0) parts.push(`${handlerResult.modes} modes`);
if (handlerResult.workflows > 0) parts.push(`${handlerResult.workflows} workflows`);
if (handlerResult.tasks > 0) parts.push(`${handlerResult.tasks} tasks`);
if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`);
detail = parts.join(', ');
}
return { success: true, ide: ideName, detail, handlerResult };
} catch (error) {
console.error(chalk.red(`Failed to setup ${ideName}:`), error.message);
return { success: false, error: error.message };
await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`);
return { success: false, ide: ideName, error: error.message };
}
}
@ -254,7 +282,7 @@ class IdeManager {
const handler = this.handlers.get(ideName.toLowerCase());
if (!handler) {
console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported for custom agent installation`));
await prompts.log.warn(`IDE '${ideName}' is not yet supported for custom agent installation`);
continue;
}
@ -266,7 +294,7 @@ class IdeManager {
}
}
} catch (error) {
console.warn(chalk.yellow(`⚠️ Failed to install ${ideName} launcher: ${error.message}`));
await prompts.log.warn(`Failed to install ${ideName} launcher: ${error.message}`);
}
}

View File

@ -111,12 +111,14 @@ platforms:
description: "AI coding platform"
# No installer config - uses custom kilo.js (creates .kilocodemodes file)
kiro-cli:
name: "Kiro CLI"
kiro:
name: "Kiro"
preferred: false
category: cli
description: "Kiro command-line interface"
# No installer config - uses custom kiro-cli.js (YAML→JSON conversion)
category: ide
description: "Amazon's AI-powered IDE"
installer:
target_dir: .kiro/steering
template_type: kiro
opencode:
name: "OpenCode"

View File

@ -1,6 +1,5 @@
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
/**

View File

@ -1,7 +1,6 @@
const path = require('node:path');
const fs = require('fs-extra');
const csv = require('csv-parse/sync');
const chalk = require('chalk');
const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils');
/**

View File

@ -1,7 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const csv = require('csv-parse/sync');
const chalk = require('chalk');
const prompts = require('../../../../lib/prompts');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
/**
@ -22,7 +22,7 @@ class WorkflowCommandGenerator {
const workflows = await this.loadWorkflowManifest(bmadDir);
if (!workflows) {
console.log(chalk.yellow('Workflow manifest not found. Skipping command generation.'));
await prompts.log.warn('Workflow manifest not found. Skipping command generation.');
return { generated: 0 };
}
@ -157,8 +157,7 @@ class WorkflowCommandGenerator {
.replaceAll('{{module}}', workflow.module)
.replaceAll('{{description}}', workflow.description)
.replaceAll('{{workflow_path}}', workflowPath)
.replaceAll('_bmad', this.bmadFolderName)
.replaceAll('_bmad', '_bmad');
.replaceAll('_bmad', this.bmadFolderName);
}
/**
@ -238,15 +237,15 @@ When running any workflow:
const match = workflowPath.match(/\/src\/bmm\/(.+)/);
if (match) {
transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`;
} else if (workflowPath.includes('/src/core/')) {
const match = workflowPath.match(/\/src\/core\/(.+)/);
if (match) {
transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`;
}
}
return transformed;
} else if (workflowPath.includes('/src/core/')) {
const match = workflowPath.match(/\/src\/core\/(.+)/);
if (match) {
transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`;
}
}
return transformed;
}
async loadWorkflowManifest(bmadDir) {

View File

@ -0,0 +1,16 @@
---
inclusion: manual
---
# {{name}}
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
<agent-activation CRITICAL="TRUE">
1. LOAD the FULL agent file from #[[file:{{bmadFolderName}}/{{path}}]]
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
3. FOLLOW every step in the <activation> section precisely
4. DISPLAY the welcome/greeting as instructed
5. PRESENT the numbered menu
6. WAIT for user input before proceeding
</agent-activation>

View File

@ -0,0 +1,9 @@
---
inclusion: manual
---
# {{name}}
Read the entire task file at: #[[file:{{bmadFolderName}}/{{path}}]]
Follow all instructions in the task file exactly as written.

View File

@ -0,0 +1,9 @@
---
inclusion: manual
---
# {{name}}
Read the entire tool file at: #[[file:{{bmadFolderName}}/{{path}}]]
Follow all instructions in the tool file exactly as written.

View File

@ -0,0 +1,15 @@
---
inclusion: manual
---
# {{name}}
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded:
<steps CRITICAL="TRUE">
1. Always LOAD the FULL #[[file:{{bmadFolderName}}/core/tasks/workflow.xml]]
2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config #[[file:{{bmadFolderName}}/{{path}}]]
3. Pass the yaml path {{bmadFolderName}}/{{path}} as 'workflow-config' parameter to the workflow.xml instructions
4. Follow workflow.xml instructions EXACTLY as written to process and follow the specific workflow config and its instructions
5. Save outputs after EACH section when generating any documents from templates
</steps>

View File

@ -0,0 +1,7 @@
---
inclusion: manual
---
# {{name}}
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL #[[file:{{bmadFolderName}}/{{path}}]], READ its entire contents and follow its directions exactly!

View File

@ -1,7 +1,7 @@
const fs = require('fs-extra');
const path = require('node:path');
const yaml = require('yaml');
const chalk = require('chalk');
const prompts = require('../../lib/prompts');
/**
* Load and display installer messages from messages.yaml
@ -51,22 +51,20 @@ class MessageLoader {
/**
* Display the start message (after logo, before prompts)
*/
displayStartMessage() {
async displayStartMessage() {
const message = this.getStartMessage();
if (message) {
console.log(chalk.cyan(message));
console.log();
await prompts.log.info(message);
}
}
/**
* Display the end message (after installation completes)
*/
displayEndMessage() {
async displayEndMessage() {
const message = this.getEndMessage();
if (message) {
console.log();
console.log(chalk.cyan(message));
await prompts.log.info(message);
}
}

View File

@ -1,8 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
const chalk = require('chalk');
const ora = require('ora');
const prompts = require('../../../lib/prompts');
const { XmlHandler } = require('../../../lib/xml-handler');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { filterCustomizationData } = require('../../../lib/agent/compiler');
@ -17,7 +16,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
* @class ModuleManager
* @requires fs-extra
* @requires yaml
* @requires chalk
* @requires prompts
* @requires XmlHandler
*
* @example
@ -152,26 +151,26 @@ class ModuleManager {
// File hasn't been modified by user, safe to update
await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Updated sidecar file: ${relativeToBmad}`));
await prompts.log.message(` Updated sidecar file: ${relativeToBmad}`);
}
} else {
// User has modified the file, preserve it
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Preserving user-modified file: ${relativeToBmad}`));
await prompts.log.message(` Preserving user-modified file: ${relativeToBmad}`);
}
}
} else {
// First time seeing this file in manifest, copy it
await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Added new sidecar file: ${relativeToBmad}`));
await prompts.log.message(` Added new sidecar file: ${relativeToBmad}`);
}
}
} else {
// New installation
await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Copied sidecar file: ${relativeToBmad}`));
await prompts.log.message(` Copied sidecar file: ${relativeToBmad}`);
}
}
@ -237,17 +236,11 @@ class ModuleManager {
async getModuleInfo(modulePath, defaultName, sourceDescription) {
// Check for module structure (module.yaml OR custom.yaml)
const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
let configPath = null;
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath;
} else if (await fs.pathExists(rootCustomConfigPath)) {
configPath = rootCustomConfigPath;
}
@ -269,7 +262,7 @@ class ModuleManager {
description: 'BMAD Module',
version: '5.0.0',
source: sourceDescription,
isCustom: configPath === customConfigPath || configPath === rootCustomConfigPath || isCustomSource,
isCustom: configPath === rootCustomConfigPath || isCustomSource,
};
// Read module config for metadata
@ -288,7 +281,7 @@ class ModuleManager {
moduleInfo.dependencies = config.dependencies || [];
moduleInfo.defaultSelected = config.default_selected === undefined ? false : config.default_selected;
} catch (error) {
console.warn(`Failed to read config for ${defaultName}:`, error.message);
await prompts.log.warn(`Failed to read config for ${defaultName}: ${error.message}`);
}
return moduleInfo;
@ -299,7 +292,7 @@ class ModuleManager {
* @param {string} moduleCode - Code of the module to find (from module.yaml)
* @returns {string|null} Path to the module source or null if not found
*/
async findModuleSource(moduleCode) {
async findModuleSource(moduleCode, options = {}) {
const projectRoot = getProjectRoot();
// First check custom module paths if they exist
@ -316,7 +309,7 @@ class ModuleManager {
}
// Check external official modules
const externalSource = await this.findExternalModuleSource(moduleCode);
const externalSource = await this.findExternalModuleSource(moduleCode, options);
if (externalSource) {
return externalSource;
}
@ -348,7 +341,7 @@ class ModuleManager {
* @param {string} moduleCode - Code of the external module
* @returns {string} Path to the cloned repository
*/
async cloneExternalModule(moduleCode) {
async cloneExternalModule(moduleCode, options = {}) {
const { execSync } = require('node:child_process');
const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode);
@ -358,10 +351,32 @@ class ModuleManager {
const cacheDir = this.getExternalCacheDir();
const moduleCacheDir = path.join(cacheDir, moduleCode);
const silent = options.silent || false;
// Create cache directory if it doesn't exist
await fs.ensureDir(cacheDir);
// Helper to create a spinner or a no-op when silent
const createSpinner = async () => {
if (silent) {
return {
start() {},
stop() {},
error() {},
message() {},
cancel() {},
clear() {},
get isSpinning() {
return false;
},
get isCancelled() {
return false;
},
};
}
return await prompts.spinner();
};
// Track if we need to install dependencies
let needsDependencyInstall = false;
let wasNewClone = false;
@ -369,21 +384,30 @@ class ModuleManager {
// Check if already cloned
if (await fs.pathExists(moduleCacheDir)) {
// Try to update if it's a git repo
const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start();
const fetchSpinner = await createSpinner();
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
try {
const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
// Fetch and reset to remote - works better with shallow clones than pull
execSync('git fetch origin --depth 1', { cwd: moduleCacheDir, stdio: 'pipe' });
execSync('git reset --hard origin/HEAD', { cwd: moduleCacheDir, stdio: 'pipe' });
execSync('git fetch origin --depth 1', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
execSync('git reset --hard origin/HEAD', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
fetchSpinner.succeed(`Fetched ${moduleInfo.name}`);
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
// Force dependency install if we got new code
if (currentRef !== newRef) {
needsDependencyInstall = true;
}
} catch {
fetchSpinner.warn(`Fetch failed, re-downloading ${moduleInfo.name}`);
fetchSpinner.error(`Fetch failed, re-downloading ${moduleInfo.name}`);
// If update fails, remove and re-clone
await fs.remove(moduleCacheDir);
wasNewClone = true;
@ -394,14 +418,16 @@ class ModuleManager {
// Clone if not exists or was removed
if (wasNewClone) {
const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start();
const fetchSpinner = await createSpinner();
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
try {
execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, {
stdio: 'pipe',
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
fetchSpinner.succeed(`Fetched ${moduleInfo.name}`);
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
} catch (error) {
fetchSpinner.fail(`Failed to fetch ${moduleInfo.name}`);
fetchSpinner.error(`Failed to fetch ${moduleInfo.name}`);
throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`);
}
}
@ -415,17 +441,18 @@ class ModuleManager {
// Force install if we updated or cloned new
if (needsDependencyInstall || wasNewClone || nodeModulesMissing) {
const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
const installSpinner = await createSpinner();
installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
try {
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
cwd: moduleCacheDir,
stdio: 'pipe',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 120_000, // 2 minute timeout
});
installSpinner.succeed(`Installed dependencies for ${moduleInfo.name}`);
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) {
installSpinner.warn(`Failed to install dependencies for ${moduleInfo.name}`);
console.warn(chalk.yellow(` Warning: ${error.message}`));
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` Warning: ${error.message}`);
}
} else {
// Check if package.json is newer than node_modules
@ -440,17 +467,18 @@ class ModuleManager {
}
if (packageJsonNewer) {
const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
const installSpinner = await createSpinner();
installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
try {
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
cwd: moduleCacheDir,
stdio: 'pipe',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 120_000, // 2 minute timeout
});
installSpinner.succeed(`Installed dependencies for ${moduleInfo.name}`);
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) {
installSpinner.warn(`Failed to install dependencies for ${moduleInfo.name}`);
console.warn(chalk.yellow(` Warning: ${error.message}`));
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` Warning: ${error.message}`);
}
}
}
@ -464,7 +492,7 @@ class ModuleManager {
* @param {string} moduleCode - Code of the external module
* @returns {string|null} Path to the module source or null if not found
*/
async findExternalModuleSource(moduleCode) {
async findExternalModuleSource(moduleCode, options = {}) {
const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode);
if (!moduleInfo) {
@ -472,7 +500,7 @@ class ModuleManager {
}
// Clone the external module repo
const cloneDir = await this.cloneExternalModule(moduleCode);
const cloneDir = await this.cloneExternalModule(moduleCode, options);
// The module-definition specifies the path to module.yaml relative to repo root
// We need to return the directory containing module.yaml
@ -493,7 +521,7 @@ class ModuleManager {
* @param {Object} options.logger - Logger instance for output
*/
async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) {
const sourcePath = await this.findModuleSource(moduleName);
const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
const targetPath = path.join(bmadDir, moduleName);
// Check if source module exists
@ -507,21 +535,13 @@ class ModuleManager {
// Check if this is a custom module and read its custom.yaml values
let customConfig = null;
const rootCustomConfigPath = path.join(sourcePath, 'custom.yaml');
const moduleInstallerCustomPath = path.join(sourcePath, '_module-installer', 'custom.yaml');
if (await fs.pathExists(rootCustomConfigPath)) {
try {
const customContent = await fs.readFile(rootCustomConfigPath, 'utf8');
customConfig = yaml.parse(customContent);
} catch (error) {
console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message));
}
} else if (await fs.pathExists(moduleInstallerCustomPath)) {
try {
const customContent = await fs.readFile(moduleInstallerCustomPath, 'utf8');
customConfig = yaml.parse(customContent);
} catch (error) {
console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message));
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`);
}
}
@ -529,7 +549,7 @@ class ModuleManager {
if (customConfig) {
options.moduleConfig = { ...options.moduleConfig, ...customConfig };
if (options.logger) {
options.logger.log(chalk.cyan(` Merged custom configuration for ${moduleName}`));
options.logger.log(` Merged custom configuration for ${moduleName}`);
}
}
@ -551,9 +571,9 @@ class ModuleManager {
// Process agent files to inject activation block
await this.processAgentFiles(targetPath, moduleName);
// Call module-specific installer if it exists (unless explicitly skipped)
// Create directories declared in module.yaml (unless explicitly skipped)
if (!options.skipModuleInstaller) {
await this.runModuleInstaller(moduleName, bmadDir, options);
await this.createModuleDirectories(moduleName, bmadDir, options);
}
// Capture version info for manifest
@ -582,7 +602,7 @@ class ModuleManager {
* @param {string} bmadDir - Target bmad directory
* @param {boolean} force - Force update (overwrite modifications)
*/
async update(moduleName, bmadDir, force = false) {
async update(moduleName, bmadDir, force = false, options = {}) {
const sourcePath = await this.findModuleSource(moduleName);
const targetPath = path.join(bmadDir, moduleName);
@ -599,7 +619,7 @@ class ModuleManager {
if (force) {
// Force update - remove and reinstall
await fs.remove(targetPath);
return await this.install(moduleName, bmadDir);
return await this.install(moduleName, bmadDir, null, { installer: options.installer });
} else {
// Selective update - preserve user modifications
await this.syncModule(sourcePath, targetPath);
@ -673,7 +693,7 @@ class ModuleManager {
const config = yaml.parse(configContent);
Object.assign(moduleInfo, config);
} catch (error) {
console.warn(`Failed to read installed module config:`, error.message);
await prompts.log.warn(`Failed to read installed module config: ${error.message}`);
}
}
@ -709,8 +729,8 @@ class ModuleManager {
continue;
}
// Skip _module-installer directory - it's only needed at install time
if (file.startsWith('_module-installer/') || file === 'module.yaml') {
// Skip module.yaml at root - it's only needed at install time
if (file === 'module.yaml') {
continue;
}
@ -735,7 +755,7 @@ class ModuleManager {
// Check for localskip="true" in the agent tag
const agentMatch = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
if (agentMatch) {
console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`));
await prompts.log.message(` Skipping web-only agent: ${path.basename(file)}`);
continue; // Skip this agent
}
}
@ -768,7 +788,6 @@ class ModuleManager {
// IMPORTANT: Replace escape sequence and placeholder BEFORE parsing YAML
// Otherwise parsing will fail on the placeholder
yamlContent = yamlContent.replaceAll('_bmad', '_bmad');
yamlContent = yamlContent.replaceAll('_bmad', this.bmadFolderName);
try {
@ -838,7 +857,7 @@ class ModuleManager {
await fs.writeFile(targetFile, strippedYaml, 'utf8');
} catch {
// If anything fails, just copy the file as-is
console.warn(chalk.yellow(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`));
await prompts.log.warn(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`);
await fs.copy(sourceFile, targetFile, { overwrite: true });
}
}
@ -890,7 +909,7 @@ class ModuleManager {
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
// Only show customize creation in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
await prompts.log.message(` Created customize: ${moduleName}-${agentName}.customize.yaml`);
}
// Store original hash for modification detection
@ -990,10 +1009,10 @@ class ModuleManager {
const copiedFiles = await this.copySidecarToMemory(sourceSidecarPath, agentName, bmadMemoryPath, isUpdate, bmadDir, installer);
if (process.env.BMAD_VERBOSE_INSTALL === 'true' && copiedFiles.length > 0) {
console.log(chalk.dim(` Sidecar files processed: ${copiedFiles.length} files`));
await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`);
}
} else if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.yellow(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`));
await prompts.log.warn(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`);
}
}
@ -1012,14 +1031,12 @@ class ModuleManager {
// Only show compilation details in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
await prompts.log.message(
` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
);
}
} catch (error) {
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
await prompts.log.warn(` Failed to compile agent ${agentName}: ${error.message}`);
}
}
}
@ -1139,11 +1156,11 @@ class ModuleManager {
}
if (!workflowsVendored) {
console.log(chalk.cyan(`\n Vendoring cross-module workflows for ${moduleName}...`));
await prompts.log.info(`\n Vendoring cross-module workflows for ${moduleName}...`);
workflowsVendored = true;
}
console.log(chalk.dim(` Processing: ${agentFile}`));
await prompts.log.message(` Processing: ${agentFile}`);
for (const item of workflowInstallItems) {
const sourceWorkflowPath = item.workflow; // Where to copy FROM
@ -1155,7 +1172,7 @@ class ModuleManager {
// Or: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/);
if (!sourceMatch) {
console.warn(chalk.yellow(` Could not parse workflow path: ${sourceWorkflowPath}`));
await prompts.log.warn(` Could not parse workflow path: ${sourceWorkflowPath}`);
continue;
}
@ -1166,7 +1183,7 @@ class ModuleManager {
// Example: {project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.yaml
const installMatch = installWorkflowPath.match(/\{project-root\}\/(_bmad)\/([^/]+)\/workflows\/(.+)/);
if (!installMatch) {
console.warn(chalk.yellow(` Could not parse workflow-install path: ${installWorkflowPath}`));
await prompts.log.warn(` Could not parse workflow-install path: ${installWorkflowPath}`);
continue;
}
@ -1179,15 +1196,13 @@ class ModuleManager {
// Check if source workflow exists
if (!(await fs.pathExists(actualSourceWorkflowPath))) {
console.warn(chalk.yellow(` Source workflow not found: ${actualSourceWorkflowPath}`));
await prompts.log.warn(` Source workflow not found: ${actualSourceWorkflowPath}`);
continue;
}
// Copy the entire workflow folder
console.log(
chalk.dim(
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`,
),
await prompts.log.message(
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`,
);
await fs.ensureDir(path.dirname(actualDestWorkflowPath));
@ -1203,7 +1218,7 @@ class ModuleManager {
}
if (workflowsVendored) {
console.log(chalk.green(` ✓ Workflow vendoring complete\n`));
await prompts.log.success(` Workflow vendoring complete\n`);
}
}
@ -1225,66 +1240,106 @@ class ModuleManager {
if (updatedYaml !== yamlContent) {
await fs.writeFile(workflowYamlPath, updatedYaml, 'utf8');
console.log(chalk.dim(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`));
await prompts.log.message(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`);
}
}
/**
* Run module-specific installer if it exists
* Create directories declared in module.yaml's `directories` key
* This replaces the security-risky module installer pattern with declarative config
* @param {string} moduleName - Name of the module
* @param {string} bmadDir - Target bmad directory
* @param {Object} options - Installation options
* @param {Object} options.moduleConfig - Module configuration from config collector
* @param {Object} options.coreConfig - Core configuration
*/
async runModuleInstaller(moduleName, bmadDir, options = {}) {
async createModuleDirectories(moduleName, bmadDir, options = {}) {
const moduleConfig = options.moduleConfig || {};
const projectRoot = path.dirname(bmadDir);
// Special handling for core module - it's in src/core not src/modules
let sourcePath;
if (moduleName === 'core') {
sourcePath = getSourcePath('core');
} else {
sourcePath = await this.findModuleSource(moduleName);
sourcePath = await this.findModuleSource(moduleName, { silent: true });
if (!sourcePath) {
// No source found, skip module installer
return;
return; // No source found, skip
}
}
const installerPath = path.join(sourcePath, '_module-installer', 'installer.js');
// Check if module has a custom installer
if (!(await fs.pathExists(installerPath))) {
return; // No custom installer
// Read module.yaml to find the `directories` key
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
if (!(await fs.pathExists(moduleYamlPath))) {
return; // No module.yaml, skip
}
let moduleYaml;
try {
// Load the module installer
const moduleInstaller = require(installerPath);
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
moduleYaml = yaml.parse(yamlContent);
} catch {
return; // Invalid YAML, skip
}
if (typeof moduleInstaller.install === 'function') {
// Get project root (parent of bmad directory)
const projectRoot = path.dirname(bmadDir);
if (!moduleYaml || !moduleYaml.directories) {
return; // No directories declared, skip
}
// Prepare logger (use console if not provided)
const logger = options.logger || {
log: console.log,
error: console.error,
warn: console.warn,
};
// Get color utility for styled output
const color = await prompts.getColor();
const directories = moduleYaml.directories;
const wdsFolders = moduleYaml.wds_folders || [];
// Call the module installer
const result = await moduleInstaller.install({
projectRoot,
config: options.moduleConfig || {},
coreConfig: options.coreConfig || {},
installedIDEs: options.installedIDEs || [],
logger,
});
for (const dirRef of directories) {
// Parse variable reference like "{design_artifacts}"
const varMatch = dirRef.match(/^\{([^}]+)\}$/);
if (!varMatch) {
// Not a variable reference, skip
continue;
}
if (!result) {
console.warn(chalk.yellow(`Module installer for ${moduleName} returned false`));
const configKey = varMatch[1];
const dirValue = moduleConfig[configKey];
if (!dirValue || typeof dirValue !== 'string') {
continue; // No value or not a string, skip
}
// Strip {project-root}/ prefix if present
let dirPath = dirValue.replace(/^\{project-root\}\/?/, '');
// Handle remaining {project-root} anywhere in the path
dirPath = dirPath.replaceAll('{project-root}', '');
// Resolve to absolute path
const fullPath = path.join(projectRoot, dirPath);
// Validate path is within project root (prevent directory traversal)
const normalizedPath = path.normalize(fullPath);
const normalizedRoot = path.normalize(projectRoot);
if (!normalizedPath.startsWith(normalizedRoot + path.sep) && normalizedPath !== normalizedRoot) {
await prompts.log.warn(color.yellow(`Warning: ${configKey} path escapes project root, skipping: ${dirPath}`));
continue;
}
// Create directory if it doesn't exist
if (!(await fs.pathExists(fullPath))) {
const dirName = configKey.replaceAll('_', ' ');
await prompts.log.message(color.yellow(`Creating ${dirName} directory: ${dirPath}`));
await fs.ensureDir(fullPath);
}
// Create WDS subfolders if this is the design_artifacts directory
if (configKey === 'design_artifacts' && wdsFolders.length > 0) {
await prompts.log.message(color.cyan('Creating WDS folder structure...'));
for (const subfolder of wdsFolders) {
const subPath = path.join(fullPath, subfolder);
if (!(await fs.pathExists(subPath))) {
await fs.ensureDir(subPath);
await prompts.log.message(color.dim(`${subfolder}/`));
}
}
}
} catch (error) {
console.error(chalk.red(`Error running module installer for ${moduleName}: ${error.message}`));
}
}
@ -1306,7 +1361,7 @@ class ModuleManager {
await fs.writeFile(configPath, configContent, 'utf8');
} catch (error) {
console.warn(`Failed to process module config:`, error.message);
await prompts.log.warn(`Failed to process module config: ${error.message}`);
}
}
}
@ -1354,10 +1409,6 @@ class ModuleManager {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip _module-installer directories
if (entry.name === '_module-installer') {
continue;
}
const subFiles = await this.getFileList(fullPath, baseDir);
files.push(...subFiles);
} else {

View File

@ -6,7 +6,7 @@
const fs = require('node:fs');
const path = require('node:path');
const yaml = require('yaml');
const readline = require('node:readline');
const prompts = require('../prompts');
const { compileAgent, compileAgentFile } = require('./compiler');
const { extractInstallConfig, getDefaultValues } = require('./template-engine');
@ -149,83 +149,47 @@ async function promptInstallQuestions(installConfig, defaults, presetAnswers = {
return { ...defaults, ...presetAnswers };
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const question = (prompt) =>
new Promise((resolve) => {
rl.question(prompt, resolve);
});
const answers = { ...defaults, ...presetAnswers };
console.log('\n📝 Agent Configuration\n');
if (installConfig.description) {
console.log(` ${installConfig.description}\n`);
}
await prompts.note(installConfig.description || '', 'Agent Configuration');
for (const q of installConfig.questions) {
// Skip questions for variables that are already set (e.g., custom_name set upfront)
if (answers[q.var] !== undefined && answers[q.var] !== defaults[q.var]) {
console.log(chalk.dim(` ${q.var}: ${answers[q.var]} (already set)`));
await prompts.log.message(` ${q.var}: ${answers[q.var]} (already set)`);
continue;
}
let response;
switch (q.type) {
case 'text': {
const defaultHint = q.default ? ` (default: ${q.default})` : '';
response = await question(` ${q.prompt}${defaultHint}: `);
answers[q.var] = response || q.default || '';
const response = await prompts.text({
message: q.prompt,
default: q.default ?? '',
});
answers[q.var] = response ?? q.default ?? '';
break;
}
case 'boolean': {
const defaultHint = q.default ? ' [Y/n]' : ' [y/N]';
response = await question(` ${q.prompt}${defaultHint}: `);
if (response === '') {
answers[q.var] = q.default;
} else {
answers[q.var] = response.toLowerCase().startsWith('y');
}
const response = await prompts.confirm({
message: q.prompt,
default: q.default,
});
answers[q.var] = response;
break;
}
case 'choice': {
console.log(` ${q.prompt}`);
for (const [idx, opt] of q.options.entries()) {
const marker = opt.value === q.default ? '* ' : ' ';
console.log(` ${marker}${idx + 1}. ${opt.label}`);
}
const defaultIdx = q.options.findIndex((o) => o.value === q.default) + 1;
let validChoice = false;
let choiceIdx;
while (!validChoice) {
response = await question(` Choice (default: ${defaultIdx}): `);
if (response) {
choiceIdx = parseInt(response, 10) - 1;
if (isNaN(choiceIdx) || choiceIdx < 0 || choiceIdx >= q.options.length) {
console.log(` Invalid choice. Please enter 1-${q.options.length}`);
} else {
validChoice = true;
}
} else {
choiceIdx = defaultIdx - 1;
validChoice = true;
}
}
answers[q.var] = q.options[choiceIdx].value;
const response = await prompts.select({
message: q.prompt,
options: q.options.map((o) => ({ value: o.value, label: o.label })),
initialValue: q.default,
});
answers[q.var] = response;
break;
}
// No default
}
}
rl.close();
return answers;
}

View File

@ -1,9 +1,6 @@
const chalk = require('chalk');
const boxen = require('boxen');
const wrapAnsi = require('wrap-ansi');
const figlet = require('figlet');
const path = require('node:path');
const os = require('node:os');
const prompts = require('./prompts');
const CLIUtils = {
/**
@ -19,27 +16,32 @@ const CLIUtils = {
},
/**
* Display BMAD logo
* @param {boolean} clearScreen - Whether to clear the screen first (default: true for initial display only)
* Display BMAD logo using @clack intro + box
* @param {boolean} _clearScreen - Deprecated, ignored (no longer clears screen)
*/
displayLogo(clearScreen = true) {
if (clearScreen) {
console.clear();
}
async displayLogo(_clearScreen = true) {
const version = this.getVersion();
const color = await prompts.getColor();
// ASCII art logo
const logo = `
`;
const logo = [
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
' ██████╔╝██╔████╔██║███████║██║ ██║',
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝',
]
.map((line) => color.yellow(line))
.join('\n');
console.log(chalk.cyan(logo));
console.log(chalk.dim(` Build More, Architect Dreams`) + chalk.cyan.bold(` v${version}`) + '\n');
const tagline = ' Build More, Architect Dreams';
await prompts.box(`${logo}\n${tagline}`, `v${version}`, {
contentAlign: 'center',
rounded: true,
formatBorder: color.blue,
});
},
/**
@ -47,13 +49,8 @@ const CLIUtils = {
* @param {string} title - Section title
* @param {string} subtitle - Optional subtitle
*/
displaySection(title, subtitle = null) {
console.log('\n' + chalk.cyan('═'.repeat(80)));
console.log(chalk.cyan.bold(` ${title}`));
if (subtitle) {
console.log(chalk.dim(` ${subtitle}`));
}
console.log(chalk.cyan('═'.repeat(80)) + '\n');
async displaySection(title, subtitle = null) {
await prompts.note(subtitle || '', title);
},
/**
@ -61,25 +58,21 @@ const CLIUtils = {
* @param {string|Array} content - Content to display
* @param {Object} options - Box options
*/
displayBox(content, options = {}) {
const defaultOptions = {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
...options,
};
// Handle array content
async displayBox(content, options = {}) {
let text = content;
if (Array.isArray(content)) {
text = content.join('\n\n');
}
// Wrap text to prevent overflow
const wrapped = wrapAnsi(text, 76, { hard: true, wordWrap: true });
const color = await prompts.getColor();
const borderColor = options.borderColor || 'cyan';
const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue };
const formatBorder = colorMap[borderColor] || color.cyan;
console.log(boxen(wrapped, defaultOptions));
await prompts.box(text, options.title, {
rounded: options.borderStyle === 'round' || options.borderStyle === undefined,
formatBorder,
});
},
/**
@ -88,14 +81,9 @@ const CLIUtils = {
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
displayModuleConfigHeader(moduleName, header = null, subheader = null) {
// Simple blue banner with custom header/subheader if provided
console.log('\n' + chalk.cyan('─'.repeat(80)));
console.log(chalk.cyan(header || `Configuring ${moduleName.toUpperCase()} Module`));
if (subheader) {
console.log(chalk.dim(`${subheader}`));
}
console.log(chalk.cyan('─'.repeat(80)) + '\n');
async displayModuleConfigHeader(moduleName, header = null, subheader = null) {
const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
await prompts.note(subheader || '', title);
},
/**
@ -104,14 +92,9 @@ const CLIUtils = {
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
displayModuleNoConfig(moduleName, header = null, subheader = null) {
// Show full banner with header/subheader, just like modules with config
console.log('\n' + chalk.cyan('─'.repeat(80)));
console.log(chalk.cyan(header || `${moduleName.toUpperCase()} Module - No Custom Configuration`));
if (subheader) {
console.log(chalk.dim(`${subheader}`));
}
console.log(chalk.cyan('─'.repeat(80)) + '\n');
async displayModuleNoConfig(moduleName, header = null, subheader = null) {
const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`;
await prompts.note(subheader || '', title);
},
/**
@ -120,42 +103,33 @@ const CLIUtils = {
* @param {number} total - Total steps
* @param {string} description - Step description
*/
displayStep(current, total, description) {
async displayStep(current, total, description) {
const progress = `[${current}/${total}]`;
console.log('\n' + chalk.cyan(progress) + ' ' + chalk.bold(description));
console.log(chalk.dim('─'.repeat(80 - progress.length - 1)) + '\n');
await prompts.log.step(`${progress} ${description}`);
},
/**
* Display completion message
* @param {string} message - Completion message
*/
displayComplete(message) {
console.log(
'\n' +
boxen(chalk.green('✨ ' + message), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'green',
}),
);
async displayComplete(message) {
const color = await prompts.getColor();
await prompts.box(`\u2728 ${message}`, 'Complete', {
rounded: true,
formatBorder: color.green,
});
},
/**
* Display error message
* @param {string} message - Error message
*/
displayError(message) {
console.log(
'\n' +
boxen(chalk.red('✗ ' + message), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'red',
}),
);
async displayError(message) {
const color = await prompts.getColor();
await prompts.box(`\u2717 ${message}`, 'Error', {
rounded: true,
formatBorder: color.red,
});
},
/**
@ -163,7 +137,7 @@ const CLIUtils = {
* @param {Array} items - Items to display
* @param {string} prefix - Item prefix
*/
formatList(items, prefix = '') {
formatList(items, prefix = '\u2022') {
return items.map((item) => ` ${prefix} ${item}`).join('\n');
},
@ -178,25 +152,6 @@ const CLIUtils = {
}
},
/**
* Display table
* @param {Array} data - Table data
* @param {Object} options - Table options
*/
displayTable(data, options = {}) {
const Table = require('cli-table3');
const table = new Table({
style: {
head: ['cyan'],
border: ['dim'],
},
...options,
});
for (const row of data) table.push(row);
console.log(table.toString());
},
/**
* Display module completion message
* @param {string} moduleName - Name of the completed module

View File

@ -89,11 +89,51 @@ async function note(message, title) {
/**
* Display a spinner for async operations
* @returns {Object} Spinner controller with start, stop, message methods
* Wraps @clack/prompts spinner with isSpinning state tracking
* @returns {Object} Spinner controller with start, stop, message, error, cancel, clear, isSpinning
*/
async function spinner() {
const clack = await getClack();
return clack.spinner();
const s = clack.spinner();
let spinning = false;
return {
start: (msg) => {
if (spinning) {
s.message(msg);
} else {
spinning = true;
s.start(msg);
}
},
stop: (msg) => {
if (spinning) {
spinning = false;
s.stop(msg);
}
},
message: (msg) => {
if (spinning) s.message(msg);
},
error: (msg) => {
spinning = false;
s.error(msg);
},
cancel: (msg) => {
spinning = false;
s.cancel(msg);
},
clear: () => {
spinning = false;
s.clear();
},
get isSpinning() {
return spinning;
},
get isCancelled() {
return s.isCancelled;
},
};
}
/**
@ -190,31 +230,6 @@ async function multiselect(options) {
return result;
}
/**
* Grouped multi-select prompt for categorized options
* @param {Object} options - Prompt options
* @param {string} options.message - The question to ask
* @param {Object} options.options - Object mapping group names to arrays of choices
* @param {Array} [options.initialValues] - Array of initially selected values
* @param {boolean} [options.required=false] - Whether at least one must be selected
* @param {boolean} [options.selectableGroups=false] - Whether groups can be selected as a whole
* @returns {Promise<Array>} Array of selected values
*/
async function groupMultiselect(options) {
const clack = await getClack();
const result = await clack.groupMultiselect({
message: options.message,
options: options.options,
initialValues: options.initialValues,
required: options.required || false,
selectableGroups: options.selectableGroups || false,
});
await handleCancel(result);
return result;
}
/**
* Default filter function for autocomplete - case-insensitive label matching
* @param {string} search - Search string
@ -237,6 +252,7 @@ function defaultAutocompleteFilter(search, option) {
* @param {boolean} [options.required=false] - Whether at least one must be selected
* @param {number} [options.maxItems=5] - Maximum visible items in scrollable list
* @param {Function} [options.filter] - Custom filter function (search, option) => boolean
* @param {Array} [options.lockedValues] - Values that are always selected and cannot be toggled off
* @returns {Promise<Array>} Array of selected values
*/
async function autocompleteMultiselect(options) {
@ -245,6 +261,7 @@ async function autocompleteMultiselect(options) {
const color = await getPicocolors();
const filterFn = options.filter ?? defaultAutocompleteFilter;
const lockedSet = new Set(options.lockedValues || []);
const prompt = new core.AutocompletePrompt({
options: options.options,
@ -255,7 +272,7 @@ async function autocompleteMultiselect(options) {
return 'Please select at least one item';
}
},
initialValue: options.initialValues,
initialValue: [...new Set([...(options.initialValues || []), ...(options.lockedValues || [])])],
render() {
const barColor = this.state === 'error' ? color.yellow : color.cyan;
const bar = barColor(clack.S_BAR);
@ -280,9 +297,17 @@ async function autocompleteMultiselect(options) {
// Render option with checkbox
const renderOption = (opt, isHighlighted) => {
const isSelected = this.selectedValues.includes(opt.value);
const isLocked = lockedSet.has(opt.value);
const label = opt.label ?? String(opt.value ?? '');
const hintText = opt.hint && opt.value === this.focusedValue ? color.dim(` (${opt.hint})`) : '';
const checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE);
const hintText = opt.hint && isHighlighted ? color.dim(` (${opt.hint})`) : '';
let checkbox;
if (isLocked) {
checkbox = color.green(clack.S_CHECKBOX_SELECTED);
const lockHint = color.dim(' (always installed)');
return isHighlighted ? `${checkbox} ${label}${lockHint}` : `${checkbox} ${color.dim(label)}${lockHint}`;
}
checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE);
return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`;
};
@ -322,6 +347,18 @@ async function autocompleteMultiselect(options) {
},
});
// Prevent locked values from being toggled off
if (lockedSet.size > 0) {
const originalToggle = prompt.toggleSelected.bind(prompt);
prompt.toggleSelected = function (value) {
// If locked and already selected, skip the toggle (would deselect)
if (lockedSet.has(value) && this.selectedValues.includes(value)) {
return;
}
originalToggle(value);
};
}
// === FIX: Make SPACE always act as selection key (not search input) ===
// Override _isActionKey to treat SPACE like TAB - always an action key
// This prevents SPACE from being added to the search input
@ -335,8 +372,9 @@ async function autocompleteMultiselect(options) {
// Handle SPACE toggle when NOT navigating (internal code only handles it when isNavigating=true)
prompt.on('key', (char, key) => {
if (key && key.name === 'space' && !prompt.isNavigating && prompt.focusedValue !== undefined) {
prompt.toggleSelected(prompt.focusedValue);
if (key && key.name === 'space' && !prompt.isNavigating) {
const focused = prompt.filteredOptions[prompt.cursor];
if (focused) prompt.toggleSelected(focused.value);
}
});
// === END FIX ===
@ -520,6 +558,131 @@ const log = {
},
};
/**
* Display cancellation message
* @param {string} [message='Operation cancelled'] - The cancellation message
*/
async function cancel(message = 'Operation cancelled') {
const clack = await getClack();
clack.cancel(message);
}
/**
* Display content in a styled box
* @param {string} content - The box content
* @param {string} [title] - Optional title
* @param {Object} [options] - Box options (contentAlign, titleAlign, width, rounded, formatBorder, etc.)
*/
async function box(content, title, options) {
const clack = await getClack();
clack.box(content, title, options);
}
/**
* Create a progress bar for visualizing task completion
* @param {Object} [options] - Progress options (max, style, etc.)
* @returns {Promise<Object>} Progress controller with start, advance, stop methods
*/
async function progress(options) {
const clack = await getClack();
return clack.progress(options);
}
/**
* Create a task log for displaying scrolling subprocess output
* @param {Object} options - TaskLog options (title, limit, retainLog)
* @returns {Promise<Object>} TaskLog controller with message, success, error methods
*/
async function taskLog(options) {
const clack = await getClack();
return clack.taskLog(options);
}
/**
* File system path prompt with autocomplete
* @param {Object} options - Path options
* @param {string} options.message - The prompt message
* @param {string} [options.initialValue] - Initial path value
* @param {boolean} [options.directory=false] - Only allow directories
* @param {Function} [options.validate] - Validation function
* @returns {Promise<string>} Selected path
*/
async function pathPrompt(options) {
const clack = await getClack();
const result = await clack.path(options);
await handleCancel(result);
return result;
}
/**
* Autocomplete single-select prompt with type-ahead filtering
* @param {Object} options - Autocomplete options
* @param {string} options.message - The prompt message
* @param {Array} options.options - Array of choices [{value, label, hint?}]
* @param {string} [options.placeholder] - Placeholder text
* @param {number} [options.maxItems] - Maximum visible items
* @param {Function} [options.filter] - Custom filter function
* @returns {Promise<any>} Selected value
*/
async function autocomplete(options) {
const clack = await getClack();
const result = await clack.autocomplete(options);
await handleCancel(result);
return result;
}
/**
* Key-based instant selection prompt
* @param {Object} options - SelectKey options
* @param {string} options.message - The prompt message
* @param {Array} options.options - Array of choices [{value, label, hint?}]
* @returns {Promise<any>} Selected value
*/
async function selectKey(options) {
const clack = await getClack();
const result = await clack.selectKey(options);
await handleCancel(result);
return result;
}
/**
* Stream messages with dynamic content (for LLMs, generators, etc.)
*/
const stream = {
async info(generator) {
const clack = await getClack();
return clack.stream.info(generator);
},
async success(generator) {
const clack = await getClack();
return clack.stream.success(generator);
},
async step(generator) {
const clack = await getClack();
return clack.stream.step(generator);
},
async warn(generator) {
const clack = await getClack();
return clack.stream.warn(generator);
},
async error(generator) {
const clack = await getClack();
return clack.stream.error(generator);
},
async message(generator, options) {
const clack = await getClack();
return clack.stream.message(generator, options);
},
};
/**
* Get the color utility (picocolors instance from @clack/prompts)
* @returns {Promise<Object>} The color utility (picocolors)
*/
async function getColor() {
return await getPicocolors();
}
/**
* Execute an array of Inquirer-style questions using @clack/prompts
* This provides compatibility with dynamic question arrays
@ -619,20 +782,28 @@ async function prompt(questions) {
module.exports = {
getClack,
getColor,
handleCancel,
intro,
outro,
cancel,
note,
box,
spinner,
progress,
taskLog,
select,
multiselect,
groupMultiselect,
autocompleteMultiselect,
autocomplete,
selectKey,
confirm,
text,
path: pathPrompt,
password,
group,
tasks,
log,
stream,
prompt,
};

View File

@ -1,4 +1,3 @@
const chalk = require('chalk');
const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
@ -30,12 +29,12 @@ class UI {
* @returns {Object} Installation configuration
*/
async promptInstall(options = {}) {
CLIUtils.displayLogo();
await CLIUtils.displayLogo();
// Display version-specific start message from install-messages.yaml
const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader();
messageLoader.displayStartMessage();
await messageLoader.displayStartMessage();
// Get directory from options or prompt
let confirmedDirectory;
@ -47,7 +46,7 @@ class UI {
throw new Error(`Invalid directory: ${validation}`);
}
confirmedDirectory = expandedDir;
console.log(chalk.cyan('Using directory from command-line:'), chalk.bold(confirmedDirectory));
await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`);
} else {
confirmedDirectory = await this.getConfirmedDirectory();
}
@ -75,7 +74,7 @@ class UI {
for (const entry of entries) {
if (entry.isDirectory() && (entry.name === '.bmad' || entry.name === 'bmad')) {
hasLegacyBmadFolder = true;
legacyBmadPath = path.join(confirmedDirectory, '.bmad');
legacyBmadPath = path.join(confirmedDirectory, entry.name);
bmadDir = legacyBmadPath;
// Check if it has _cfg folder
@ -98,38 +97,30 @@ class UI {
// Handle legacy .bmad or _cfg folder - these are very old (v4 or alpha)
// Show version warning instead of offering conversion
if (hasLegacyBmadFolder || hasLegacyCfg) {
console.log('');
console.log(chalk.yellow.bold('⚠️ LEGACY INSTALLATION DETECTED'));
console.log(chalk.yellow('─'.repeat(80)));
console.log(
chalk.yellow(
'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder - this is from a old BMAD version that is out of date for automatic upgrade, manual intervention required.',
),
await prompts.log.warn('LEGACY INSTALLATION DETECTED');
await prompts.note(
'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder -\n' +
'this is from an old BMAD version that is out of date for automatic upgrade,\n' +
'manual intervention required.\n\n' +
'You have a legacy version installed (v4 or alpha).\n' +
'Legacy installations may have compatibility issues.\n\n' +
'For the best experience, we strongly recommend:\n' +
' 1. Delete your current BMAD installation folder (.bmad or bmad)\n' +
' 2. Run a fresh installation\n\n' +
'If you do not want to start fresh, you can attempt to proceed beyond this\n' +
'point IF you have ensured the bmad folder is named _bmad, and under it there\n' +
'is a _config folder. If you have a folder under your bmad folder named _cfg,\n' +
'you would need to rename it _config, and then restart the installer.\n\n' +
'Benefits of a fresh install:\n' +
' \u2022 Cleaner configuration without legacy artifacts\n' +
' \u2022 All new features properly configured\n' +
' \u2022 Fewer potential conflicts\n\n' +
'If you have already produced output from an earlier alpha version, you can\n' +
'still retain those artifacts. After installation, ensure you configured during\n' +
'install the proper file locations for artifacts depending on the module you\n' +
'are using, or move the files to the proper locations.',
'Legacy Installation Detected',
);
console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).'));
console.log('');
console.log(chalk.dim('Legacy installations may have compatibility issues.'));
console.log('');
console.log(chalk.dim('For the best experience, we strongly recommend:'));
console.log(chalk.dim(' 1. Delete your current BMAD installation folder (.bmad or bmad)'));
console.log(
chalk.dim(
' 2. Run a fresh installation\n\nIf you do not want to start fresh, you can attempt to proceed beyond this point IF you have ensured the bmad folder is named _bmad, and under it there is a _config folder. If you have a folder under your bmad folder named _cfg, you would need to rename it _config, and then restart the installer.',
),
);
console.log('');
console.log(chalk.dim('Benefits of a fresh install:'));
console.log(chalk.dim(' • Cleaner configuration without legacy artifacts'));
console.log(chalk.dim(' • All new features properly configured'));
console.log(chalk.dim(' • Fewer potential conflicts'));
console.log(chalk.dim(''));
console.log(
chalk.dim(
'If you have already produced output from an earlier alpha version, you can still retain those artifacts. After installation, ensure you configured during install the proper file locations for artifacts depending on the module you are using, or move the files to the proper locations.',
),
);
console.log(chalk.yellow('─'.repeat(80)));
console.log('');
const proceed = await prompts.select({
message: 'How would you like to proceed?',
@ -147,37 +138,33 @@ class UI {
});
if (proceed === 'cancel') {
console.log('');
console.log(chalk.cyan('To do a fresh install:'));
console.log(chalk.dim(' 1. Delete the existing bmad folder in your project'));
console.log(chalk.dim(" 2. Run 'bmad install' again"));
console.log('');
await prompts.note('1. Delete the existing bmad folder in your project\n' + "2. Run 'bmad install' again", 'To do a fresh install');
process.exit(0);
return;
}
const ora = require('ora');
const spinner = ora('Updating folder structure...').start();
const s = await prompts.spinner();
s.start('Updating folder structure...');
try {
// Handle .bmad folder
if (hasLegacyBmadFolder) {
const newBmadPath = path.join(confirmedDirectory, '_bmad');
await fs.move(legacyBmadPath, newBmadPath);
bmadDir = newBmadPath;
spinner.succeed('Renamed ".bmad" to "_bmad"');
s.stop(`Renamed "${path.basename(legacyBmadPath)}" to "_bmad"`);
}
// Handle _cfg folder (either from .bmad or standalone)
const cfgPath = path.join(bmadDir, '_cfg');
if (await fs.pathExists(cfgPath)) {
spinner.start('Renaming configuration folder...');
s.start('Renaming configuration folder...');
const newCfgPath = path.join(bmadDir, '_config');
await fs.move(cfgPath, newCfgPath);
spinner.succeed('Renamed "_cfg" to "_config"');
s.stop('Renamed "_cfg" to "_config"');
}
} catch (error) {
spinner.fail('Failed to update folder structure');
console.error(chalk.red(`Error: ${error.message}`));
s.stop('Failed to update folder structure');
await prompts.log.error(`Error: ${error.message}`);
process.exit(1);
}
}
@ -239,7 +226,7 @@ class UI {
throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`);
}
actionType = options.action;
console.log(chalk.cyan('Using action from command-line:'), chalk.bold(actionType));
await prompts.log.info(`Using action from command-line: ${actionType}`);
} else {
actionType = await prompts.select({
message: 'How would you like to proceed?',
@ -274,7 +261,7 @@ class UI {
// Get existing installation info
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`);
// Unified module selection - all modules in one grouped multiselect
let selectedModules;
@ -284,13 +271,12 @@ class UI {
.split(',')
.map((m) => m.trim())
.filter(Boolean);
console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', ')));
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
} else {
selectedModules = await this.selectAllModules(installedModuleIds);
}
// After module selection, ask about custom modules
console.log('');
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
if (options.customContent) {
@ -299,7 +285,7 @@ class UI {
.split(',')
.map((p) => p.trim())
.filter(Boolean);
console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', ')));
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
// Build custom content config similar to promptCustomContentSource
const customPaths = [];
@ -309,7 +295,7 @@ class UI {
const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath);
if (validation) {
console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`));
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
continue;
}
@ -321,12 +307,12 @@ class UI {
const yaml = require('yaml');
moduleMeta = yaml.parse(moduleYaml);
} catch (error) {
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`));
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
continue;
}
if (!moduleMeta.code) {
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`));
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
continue;
}
@ -375,6 +361,9 @@ class UI {
selectedModules.push(...customModuleResult.selectedCustomModules);
}
// Filter out core - it's always installed via installCore flag
selectedModules = selectedModules.filter((m) => m !== 'core');
// Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
@ -404,11 +393,11 @@ class UI {
.split(',')
.map((m) => m.trim())
.filter(Boolean);
console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', ')));
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
} else if (options.yes) {
// Use default modules when --yes flag is set
selectedModules = await this.getDefaultModules(installedModuleIds);
console.log(chalk.cyan('Using default modules (--yes flag):'), chalk.bold(selectedModules.join(', ')));
await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`);
} else {
selectedModules = await this.selectAllModules(installedModuleIds);
}
@ -420,7 +409,7 @@ class UI {
.split(',')
.map((p) => p.trim())
.filter(Boolean);
console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', ')));
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
// Build custom content config similar to promptCustomContentSource
const customPaths = [];
@ -430,7 +419,7 @@ class UI {
const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath);
if (validation) {
console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`));
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
continue;
}
@ -442,12 +431,12 @@ class UI {
const yaml = require('yaml');
moduleMeta = yaml.parse(moduleYaml);
} catch (error) {
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`));
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
continue;
}
if (!moduleMeta.code) {
console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`));
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
continue;
}
@ -531,7 +520,7 @@ class UI {
const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value));
const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id));
if (unknownTools.length > 0) {
console.log(chalk.yellow(`⚠️ Previously configured tools are no longer available: ${unknownTools.join(', ')}`));
await prompts.log.warn(`Previously configured tools are no longer available: ${unknownTools.join(', ')}`);
}
// ─────────────────────────────────────────────────────────────────────────────
@ -569,21 +558,20 @@ class UI {
const selectedIdes = upgradeSelected || [];
if (selectedIdes.length === 0) {
console.log('');
const confirmNoTools = await prompts.confirm({
message: 'No tools selected. Continue without installing any tools?',
default: false,
});
if (!confirmNoTools) {
return this.promptToolSelection(projectDir);
return this.promptToolSelection(projectDir, options);
}
return { ides: [], skipIde: true };
}
// Display selected tools
this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false };
}
@ -609,25 +597,25 @@ class UI {
if (options.tools) {
// Check for explicit "none" value to skip tool installation
if (options.tools.toLowerCase() === 'none') {
console.log(chalk.cyan('Skipping tool configuration (--tools none)'));
await prompts.log.info('Skipping tool configuration (--tools none)');
return { ides: [], skipIde: true };
} else {
selectedIdes = options.tools
.split(',')
.map((t) => t.trim())
.filter(Boolean);
console.log(chalk.cyan('Using tools from command-line:'), chalk.bold(selectedIdes.join(', ')));
this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false };
}
} else if (options.yes) {
// If --yes flag is set, skip tool prompt and use previously configured tools or empty
if (configuredIdes.length > 0) {
console.log(chalk.cyan('Using previously configured tools (--yes flag):'), chalk.bold(configuredIdes.join(', ')));
this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
await prompts.log.info(`Using previously configured tools (--yes flag): ${configuredIdes.join(', ')}`);
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
return { ides: configuredIdes, skipIde: false };
} else {
console.log(chalk.cyan('Skipping tool configuration (--yes flag, no previous tools)'));
await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)');
return { ides: [], skipIde: true };
}
}
@ -647,7 +635,6 @@ class UI {
// STEP 3: Confirm if no tools selected
// ─────────────────────────────────────────────────────────────────────────────
if (selectedIdes.length === 0) {
console.log('');
const confirmNoTools = await prompts.confirm({
message: 'No tools selected. Continue without installing any tools?',
default: false,
@ -655,7 +642,7 @@ class UI {
if (!confirmNoTools) {
// User wants to select tools - recurse
return this.promptToolSelection(projectDir);
return this.promptToolSelection(projectDir, options);
}
return {
@ -665,7 +652,7 @@ class UI {
}
// Display selected tools
this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return {
ides: selectedIdes,
@ -708,15 +695,12 @@ class UI {
* Display installation summary
* @param {Object} result - Installation result
*/
showInstallSummary(result) {
// Clean, simple completion message
console.log('\n' + chalk.green.bold('✨ BMAD is ready to use!'));
// Show installation summary in a simple format
console.log(chalk.dim(`Installed to: ${result.path}`));
async showInstallSummary(result) {
let summary = `Installed to: ${result.path}`;
if (result.modules && result.modules.length > 0) {
console.log(chalk.dim(`Modules: ${result.modules.join(', ')}`));
summary += `\nModules: ${result.modules.join(', ')}`;
}
await prompts.note(summary, 'BMAD is ready to use!');
}
/**
@ -769,19 +753,19 @@ class UI {
const coreConfig = {};
if (options.userName) {
coreConfig.user_name = options.userName;
console.log(chalk.cyan('Using user name from command-line:'), chalk.bold(options.userName));
await prompts.log.info(`Using user name from command-line: ${options.userName}`);
}
if (options.communicationLanguage) {
coreConfig.communication_language = options.communicationLanguage;
console.log(chalk.cyan('Using communication language from command-line:'), chalk.bold(options.communicationLanguage));
await prompts.log.info(`Using communication language from command-line: ${options.communicationLanguage}`);
}
if (options.documentOutputLanguage) {
coreConfig.document_output_language = options.documentOutputLanguage;
console.log(chalk.cyan('Using document output language from command-line:'), chalk.bold(options.documentOutputLanguage));
await prompts.log.info(`Using document output language from command-line: ${options.documentOutputLanguage}`);
}
if (options.outputFolder) {
coreConfig.output_folder = options.outputFolder;
console.log(chalk.cyan('Using output folder from command-line:'), chalk.bold(options.outputFolder));
await prompts.log.info(`Using output folder from command-line: ${options.outputFolder}`);
}
// Load existing config to merge with provided options
@ -818,7 +802,7 @@ class UI {
document_output_language: 'English',
output_folder: '_bmad-output',
};
console.log(chalk.cyan('Using default configuration (--yes flag)'));
await prompts.log.info('Using default configuration (--yes flag)');
}
} else {
// Load existing configs first if they exist
@ -839,11 +823,11 @@ class UI {
* @returns {Array} Module choices for prompt
*/
async getModuleChoices(installedModuleIds, customContentConfig = null) {
const color = await prompts.getColor();
const moduleChoices = [];
const isNewInstallation = installedModuleIds.size === 0;
const customContentItems = [];
const hasCustomContentItems = false;
// Add custom content items
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
@ -855,7 +839,7 @@ class UI {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo) {
customContentItems.push({
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`,
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
checked: true, // Default to selected since user chose to provide custom content
path: customInfo.path, // Track path to avoid duplicates
@ -883,7 +867,7 @@ class UI {
if (!isDuplicate) {
allCustomModules.push({
name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(cached)`)}`,
name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
hint: mod.description || undefined,
@ -917,112 +901,10 @@ class UI {
}
/**
* Prompt for module selection
* @param {Array} moduleChoices - Available module choices
* @returns {Array} Selected module IDs
*/
async selectModules(moduleChoices, defaultSelections = null) {
// If defaultSelections is provided, use it to override checked state
// Otherwise preserve the checked state from moduleChoices (set by getModuleChoices)
const choicesWithDefaults = moduleChoices.map((choice) => ({
...choice,
...(defaultSelections === null ? {} : { checked: defaultSelections.includes(choice.value) }),
}));
// Add a "None" option at the end for users who changed their mind
const choicesWithSkipOption = [
...choicesWithDefaults,
{
value: '__NONE__',
label: '⚠ None / I changed my mind - skip module installation',
checked: false,
},
];
const selected = await prompts.multiselect({
message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
choices: choicesWithSkipOption,
required: true,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
console.log();
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no modules will be installed.'));
console.log();
return [];
}
// Filter out the special '__NONE__' value
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
* Get external module choices for selection
* @returns {Array} External module choices for prompt
*/
async getExternalModuleChoices() {
const externalManager = new ExternalModuleManager();
const modules = await externalManager.listAvailable();
return modules.map((mod) => ({
name: mod.name,
value: mod.code, // Use the code (e.g., 'cis') as the value
checked: mod.defaultSelected || false,
hint: mod.description || undefined, // Show description as hint
module: mod, // Store full module info for later use
}));
}
/**
* Prompt for external module selection
* @param {Array} externalModuleChoices - Available external module choices
* @param {Array} defaultSelections - Module codes to pre-select
* @returns {Array} Selected external module codes
*/
async selectExternalModules(externalModuleChoices, defaultSelections = []) {
// Build a message showing available modules
const availableNames = externalModuleChoices.map((c) => c.name).join(', ');
const message = `Select official BMad modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`;
// Mark choices as checked based on defaultSelections
const choicesWithDefaults = externalModuleChoices.map((choice) => ({
...choice,
checked: defaultSelections.includes(choice.value),
}));
// Add a "None" option at the end for users who changed their mind
const choicesWithSkipOption = [
...choicesWithDefaults,
{
name: '⚠ None / I changed my mind - skip external module installation',
value: '__NONE__',
checked: false,
},
];
const selected = await prompts.multiselect({
message,
choices: choicesWithSkipOption,
required: true,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
console.log();
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no external modules will be installed.'));
console.log();
return [];
}
// Filter out the special '__NONE__' value
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
* Select all modules (core + official + community) using grouped multiselect
* Select all modules (official + community) using grouped multiselect.
* Core is shown as locked but filtered from the result since it's always installed separately.
* @param {Set} installedModuleIds - Currently installed module IDs
* @returns {Array} Selected module codes
* @returns {Array} Selected module codes (excluding core)
*/
async selectAllModules(installedModuleIds = new Set()) {
const { ModuleManager } = require('../installers/lib/modules/manager');
@ -1033,100 +915,87 @@ class UI {
const externalManager = new ExternalModuleManager();
const externalModules = await externalManager.listAvailable();
// Build grouped options
const groupedOptions = {};
// Build flat options list with group hints for autocompleteMultiselect
const allOptions = [];
const initialValues = [];
const lockedValues = ['core'];
// Core module is always installed — show it locked at the top
allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' });
initialValues.push('core');
// Helper to build module entry with proper sorting and selection
const buildModuleEntry = (mod, value) => {
const buildModuleEntry = (mod, value, group) => {
const isInstalled = installedModuleIds.has(value);
const isDefault = mod.defaultSelected === true;
return {
label: mod.description ? `${mod.name}${mod.description}` : mod.name,
label: mod.name,
value,
// For sorting: defaultSelected=0, others=1
sortKey: isDefault ? 0 : 1,
// Pre-select if default selected OR already installed
selected: isDefault || isInstalled,
hint: mod.description || group,
// Pre-select only if already installed (not on fresh install)
selected: isInstalled,
};
};
// Group 1: BMad Core (BMM, BMB)
const coreModules = [];
// Local modules (BMM, BMB, etc.)
const localEntries = [];
for (const mod of localModules) {
if (!mod.isCustom && (mod.id === 'bmm' || mod.id === 'bmb')) {
const entry = buildModuleEntry(mod, mod.id);
coreModules.push(entry);
if (!mod.isCustom && mod.id !== 'core') {
const entry = buildModuleEntry(mod, mod.id, 'Local');
localEntries.push(entry);
if (entry.selected) {
initialValues.push(mod.id);
}
}
}
// Sort: defaultSelected first, then others
coreModules.sort((a, b) => a.sortKey - b.sortKey);
// Remove sortKey from final entries
if (coreModules.length > 0) {
groupedOptions['BMad Core'] = coreModules.map(({ label, value }) => ({ label, value }));
}
allOptions.push(...localEntries.map(({ label, value, hint }) => ({ label, value, hint })));
// Group 2: BMad Official Modules (type: bmad-org)
const officialModules = [];
for (const mod of externalModules) {
if (mod.type === 'bmad-org') {
const entry = buildModuleEntry(mod, mod.code);
const entry = buildModuleEntry(mod, mod.code, 'Official');
officialModules.push(entry);
if (entry.selected) {
initialValues.push(mod.code);
}
}
}
officialModules.sort((a, b) => a.sortKey - b.sortKey);
if (officialModules.length > 0) {
groupedOptions['BMad Official Modules'] = officialModules.map(({ label, value }) => ({ label, value }));
}
allOptions.push(...officialModules.map(({ label, value, hint }) => ({ label, value, hint })));
// Group 3: Community Modules (type: community)
const communityModules = [];
for (const mod of externalModules) {
if (mod.type === 'community') {
const entry = buildModuleEntry(mod, mod.code);
const entry = buildModuleEntry(mod, mod.code, 'Community');
communityModules.push(entry);
if (entry.selected) {
initialValues.push(mod.code);
}
}
}
communityModules.sort((a, b) => a.sortKey - b.sortKey);
if (communityModules.length > 0) {
groupedOptions['Community Modules'] = communityModules.map(({ label, value }) => ({ label, value }));
}
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));
// Add "None" option at the end
groupedOptions[' '] = [
{
label: '⚠ None - Skip module installation',
value: '__NONE__',
},
];
const selected = await prompts.groupMultiselect({
message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
options: groupedOptions,
const selected = await prompts.autocompleteMultiselect({
message: 'Select modules to install:',
options: allOptions,
initialValues: initialValues.length > 0 ? initialValues : undefined,
lockedValues,
required: true,
selectableGroups: false,
maxItems: allOptions.length,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
console.log();
console.log(chalk.yellow('⚠️ "None" was selected, so no modules will be installed.'));
console.log();
return [];
const result = selected ? selected.filter((m) => m !== 'core') : [];
// Display selected modules as bulleted list
if (result.length > 0) {
const moduleLines = result.map((moduleId) => {
const opt = allOptions.find((o) => o.value === moduleId);
return ` \u2022 ${opt?.label || moduleId}`;
});
await prompts.log.message('Selected modules:\n' + moduleLines.join('\n'));
}
// Filter out the special '__NONE__' value
return selected ? selected.filter((m) => m !== '__NONE__') : [];
return result;
}
/**
@ -1185,7 +1054,7 @@ class UI {
* @param {string} directory - The directory path
*/
async displayDirectoryInfo(directory) {
console.log(chalk.cyan('\nResolved installation path:'), chalk.bold(directory));
await prompts.log.info(`Resolved installation path: ${directory}`);
const dirExists = await fs.pathExists(directory);
if (dirExists) {
@ -1201,12 +1070,10 @@ class UI {
const hasBmadInstall =
(await fs.pathExists(bmadResult.bmadDir)) && (await fs.pathExists(path.join(bmadResult.bmadDir, '_config', 'manifest.yaml')));
console.log(
chalk.gray(`Directory exists and contains ${files.length} item(s)`) +
(hasBmadInstall ? chalk.yellow(` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})`) : ''),
);
const bmadNote = hasBmadInstall ? ` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})` : '';
await prompts.log.message(`Directory exists and contains ${files.length} item(s)${bmadNote}`);
} else {
console.log(chalk.gray('Directory exists and is empty'));
await prompts.log.message('Directory exists and is empty');
}
}
}
@ -1227,7 +1094,7 @@ class UI {
});
if (!proceed) {
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
await prompts.log.warn("Let's try again with a different path.");
}
return proceed;
@ -1239,7 +1106,7 @@ class UI {
});
if (!create) {
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
await prompts.log.warn("Let's try again with a different path.");
}
return create;
@ -1459,7 +1326,7 @@ class UI {
return configs;
} catch {
// If loading fails, return empty configs
console.warn('Warning: Could not load existing configurations');
await prompts.log.warn('Could not load existing configurations');
return configs;
}
}
@ -1590,7 +1457,7 @@ class UI {
name: moduleData.name || moduleData.code,
});
console.log(chalk.green(`Confirmed local custom module: ${moduleData.name || moduleData.code}`));
await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`);
}
// Ask if user wants to add these to the installation
@ -1656,11 +1523,11 @@ class UI {
};
// Ask user about custom modules
console.log(chalk.cyan('\n⚙ Custom Modules'));
await prompts.log.info('Custom Modules');
if (cachedCustomModules.length > 0) {
console.log(chalk.dim('Found custom modules in your installation:'));
await prompts.log.message('Found custom modules in your installation:');
} else {
console.log(chalk.dim('No custom modules currently installed.'));
await prompts.log.message('No custom modules currently installed.');
}
// Build choices dynamically based on whether we have existing modules
@ -1686,14 +1553,14 @@ class UI {
case 'keep': {
// Keep all existing custom modules
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
console.log(chalk.dim(`Keeping ${result.selectedCustomModules.length} custom module(s)`));
await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`);
break;
}
case 'select': {
// Let user choose which to keep
const selectChoices = cachedCustomModules.map((m) => ({
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
name: `${m.name} (${m.id})`,
value: m.id,
checked: m.checked,
}));
@ -1709,16 +1576,14 @@ class UI {
];
const keepModules = await prompts.multiselect({
message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
message: 'Select custom modules to keep (use arrow keys, space to toggle):',
choices: choicesWithSkip,
required: true,
});
// If user selected both "__NONE__" and other modules, honor the "None" choice
if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) {
console.log();
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no custom modules will be kept.'));
console.log();
await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.');
result.selectedCustomModules = [];
} else {
// Filter out the special '__NONE__' value
@ -1743,13 +1608,13 @@ class UI {
case 'remove': {
// Remove all custom modules
console.log(chalk.yellow('All custom modules will be removed from the installation'));
await prompts.log.warn('All custom modules will be removed from the installation');
break;
}
case 'cancel': {
// User cancelled - no custom modules
console.log(chalk.dim('No custom modules will be added'));
await prompts.log.message('No custom modules will be added');
break;
}
}
@ -1782,30 +1647,26 @@ class UI {
return true; // Not legacy, proceed
}
console.log('');
console.log(chalk.yellow.bold('⚠️ VERSION WARNING'));
console.log(chalk.yellow('─'.repeat(80)));
let warningContent;
if (installedVersion === 'unknown') {
console.log(chalk.yellow('Unable to detect your installed BMAD version.'));
console.log(chalk.yellow('This appears to be a legacy or unsupported installation.'));
warningContent = 'Unable to detect your installed BMAD version.\n' + 'This appears to be a legacy or unsupported installation.';
} else {
console.log(chalk.yellow(`You are updating from ${installedVersion} to ${currentVersion}.`));
console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).'));
warningContent =
`You are updating from ${installedVersion} to ${currentVersion}.\n` + 'You have a legacy version installed (v4 or alpha).';
}
console.log('');
console.log(chalk.dim('For the best experience, we recommend:'));
console.log(chalk.dim(' 1. Delete your current BMAD installation folder'));
console.log(chalk.dim(` (the "${bmadFolderName}/" folder in your project)`));
console.log(chalk.dim(' 2. Run a fresh installation'));
console.log('');
console.log(chalk.dim('Benefits of a fresh install:'));
console.log(chalk.dim(' • Cleaner configuration without legacy artifacts'));
console.log(chalk.dim(' • All new features properly configured'));
console.log(chalk.dim(' • Fewer potential conflicts'));
console.log(chalk.yellow('─'.repeat(80)));
console.log('');
warningContent +=
'\n\nFor the best experience, we recommend:\n' +
' 1. Delete your current BMAD installation folder\n' +
` (the "${bmadFolderName}/" folder in your project)\n` +
' 2. Run a fresh installation\n\n' +
'Benefits of a fresh install:\n' +
' \u2022 Cleaner configuration without legacy artifacts\n' +
' \u2022 All new features properly configured\n' +
' \u2022 Fewer potential conflicts';
await prompts.log.warn('VERSION WARNING');
await prompts.note(warningContent, 'Version Warning');
const proceed = await prompts.select({
message: 'How would you like to proceed?',
@ -1823,11 +1684,10 @@ class UI {
});
if (proceed === 'cancel') {
console.log('');
console.log(chalk.cyan('To do a fresh install:'));
console.log(chalk.dim(` 1. Delete the "${bmadFolderName}/" folder in your project`));
console.log(chalk.dim(" 2. Run 'bmad install' again"));
console.log('');
await prompts.note(
`1. Delete the "${bmadFolderName}/" folder in your project\n` + "2. Run 'bmad install' again",
'To do a fresh install',
);
}
return proceed === 'proceed';
@ -1838,41 +1698,34 @@ class UI {
* @param {Array} modules - Array of module info objects with version info
* @param {Array} availableUpdates - Array of available updates
*/
displayModuleVersions(modules, availableUpdates = []) {
console.log('');
console.log(chalk.cyan.bold('📦 Module Versions'));
console.log(chalk.gray('─'.repeat(80)));
async displayModuleVersions(modules, availableUpdates = []) {
// Group modules by source
const builtIn = modules.filter((m) => m.source === 'built-in');
const external = modules.filter((m) => m.source === 'external');
const custom = modules.filter((m) => m.source === 'custom');
const unknown = modules.filter((m) => m.source === 'unknown');
const displayGroup = (group, title) => {
const lines = [];
const formatGroup = (group, title) => {
if (group.length === 0) return;
console.log(chalk.yellow(`\n${title}`));
for (const module of group) {
const updateInfo = availableUpdates.find((u) => u.name === module.name);
const versionDisplay = module.version || chalk.gray('unknown');
lines.push(title);
for (const mod of group) {
const updateInfo = availableUpdates.find((u) => u.name === mod.name);
const versionDisplay = mod.version || 'unknown';
if (updateInfo) {
console.log(
` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay}${chalk.green(updateInfo.latestVersion)} ${chalk.green('↑')}`,
);
lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2192 ${updateInfo.latestVersion} \u2191`);
} else {
console.log(` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay} ${chalk.gray('✓')}`);
lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2713`);
}
}
};
displayGroup(builtIn, 'Built-in Modules');
displayGroup(external, 'External Modules (Official)');
displayGroup(custom, 'Custom Modules');
displayGroup(unknown, 'Other Modules');
formatGroup(builtIn, 'Built-in Modules');
formatGroup(external, 'External Modules (Official)');
formatGroup(custom, 'Custom Modules');
formatGroup(unknown, 'Other Modules');
console.log('');
await prompts.note(lines.join('\n'), 'Module Versions');
}
/**
@ -1885,12 +1738,10 @@ class UI {
return [];
}
console.log('');
console.log(chalk.cyan.bold('🔄 Available Updates'));
console.log(chalk.gray('─'.repeat(80)));
await prompts.log.info('Available Updates');
const choices = availableUpdates.map((update) => ({
name: `${update.name} ${chalk.dim(`(v${update.installedVersion} v${update.latestVersion})`)}`,
name: `${update.name} (v${update.installedVersion} \u2192 v${update.latestVersion})`,
value: update.name,
checked: true, // Default to selecting all updates
}));
@ -1916,7 +1767,7 @@ class UI {
// Allow specific selection
const selected = await prompts.multiselect({
message: `Select modules to update ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
message: 'Select modules to update (use arrow keys, space to toggle):',
choices: choices,
required: true,
});
@ -1928,34 +1779,29 @@ class UI {
* Display status of all installed modules
* @param {Object} statusData - Status data with modules, installation info, and available updates
*/
displayStatus(statusData) {
async displayStatus(statusData) {
const { installation, modules, availableUpdates, bmadDir } = statusData;
console.log('');
console.log(chalk.cyan.bold('📋 BMAD Status'));
console.log(chalk.gray('─'.repeat(80)));
// Installation info
console.log(chalk.yellow('\nInstallation'));
console.log(` ${chalk.gray('Version:'.padEnd(20))} ${installation.version || chalk.gray('unknown')}`);
console.log(` ${chalk.gray('Location:'.padEnd(20))} ${bmadDir}`);
console.log(` ${chalk.gray('Installed:'.padEnd(20))} ${new Date(installation.installDate).toLocaleDateString()}`);
console.log(
` ${chalk.gray('Last Updated:'.padEnd(20))} ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : chalk.gray('unknown')}`,
);
const infoLines = [
`Version: ${installation.version || 'unknown'}`,
`Location: ${bmadDir}`,
`Installed: ${new Date(installation.installDate).toLocaleDateString()}`,
`Last Updated: ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : 'unknown'}`,
];
await prompts.note(infoLines.join('\n'), 'BMAD Status');
// Module versions
this.displayModuleVersions(modules, availableUpdates);
await this.displayModuleVersions(modules, availableUpdates);
// Update summary
if (availableUpdates.length > 0) {
console.log(chalk.yellow.bold(`\n⚠️ ${availableUpdates.length} update(s) available`));
console.log(chalk.dim(` Run 'bmad install' and select "Quick Update" to update`));
await prompts.log.warn(`${availableUpdates.length} update(s) available`);
await prompts.log.message('Run \'bmad install\' and select "Quick Update" to update');
} else {
console.log(chalk.green.bold('\n✓ All modules are up to date'));
await prompts.log.success('All modules are up to date');
}
console.log('');
}
/**
@ -1964,19 +1810,17 @@ class UI {
* @param {Array} preferredIdes - Array of preferred IDE objects
* @param {Array} allTools - Array of all tool objects
*/
displaySelectedTools(selectedIdes, preferredIdes, allTools) {
async displaySelectedTools(selectedIdes, preferredIdes, allTools) {
if (selectedIdes.length === 0) return;
const preferredValues = new Set(preferredIdes.map((ide) => ide.value));
console.log('');
console.log(chalk.dim(' Selected tools:'));
for (const ideValue of selectedIdes) {
const toolLines = selectedIdes.map((ideValue) => {
const tool = allTools.find((t) => t.value === ideValue);
const name = tool?.name || ideValue;
const marker = preferredValues.has(ideValue) ? ' ⭐' : '';
console.log(chalk.dim(`${name}${marker}`));
}
const marker = preferredValues.has(ideValue) ? ' \u2B50' : '';
return ` \u2022 ${name}${marker}`;
});
await prompts.log.message('Selected tools:\n' + toolLines.join('\n'));
}
}

View File

@ -20,11 +20,8 @@ const path = require('node:path');
const DOCS_ROOT = path.resolve(__dirname, '../docs');
const DRY_RUN = !process.argv.includes('--write');
// Regex to match markdown links:
// - [text](path.md) or [text](path.md#anchor) - existing .md links
// - [text](/path/to/page/) or [text](/path/to/page/#anchor) - site-relative links to convert
const MARKDOWN_LINK_REGEX = /\[([^\]]*)\]\(([^)]+(?:\.md|\/))(?:#[^)]*)?(?:\?[^)]*)?\)/g;
// Simpler approach: match all markdown links and filter in the handler
// Match all markdown links; filtering (external, anchors, assets) happens in convertToRepoRelative.
// This intentionally matches broadly so the handler can make context-aware decisions.
const ALL_MARKDOWN_LINKS_REGEX = /\[([^\]]*)\]\(([^)]+)\)/g;
/**
@ -64,8 +61,8 @@ function getMarkdownFiles(dir) {
* @returns {string|null} - Repo-relative path (e.g., "/docs/path/to/file.md"), or null if shouldn't be converted
*/
function convertToRepoRelative(href, currentFilePath) {
// Skip external links
if (href.includes('://') || href.startsWith('mailto:') || href.startsWith('tel:')) {
// Skip external links (including protocol-relative URLs like //cdn.example.com)
if (href.includes('://') || href.startsWith('//') || href.startsWith('mailto:') || href.startsWith('tel:')) {
return null;
}

View File

@ -67,11 +67,11 @@ platforms:
category: ide
description: "Atlassian's Rovo development environment"
kiro-cli:
name: "Kiro CLI"
kiro:
name: "Kiro"
preferred: false
category: cli
description: "Kiro command-line interface"
category: ide
description: "Amazon's AI-powered IDE"
github-copilot:
name: "GitHub Copilot"

View File

@ -21,8 +21,8 @@ const path = require('node:path');
const DOCS_ROOT = path.resolve(__dirname, '../docs');
const DRY_RUN = !process.argv.includes('--write');
// Regex to match markdown links with site-relative paths
const LINK_REGEX = /\[([^\]]*)\]\((\/[^)]+)\)/g;
// Regex to match markdown links with site-relative paths or bare .md references
const LINK_REGEX = /\[([^\]]*)\]\(((?:\.{1,2}\/|\/)[^)]+|[\w][^)\s]*\.md(?:[?#][^)]*)?)\)/g;
// File extensions that are static assets, not markdown docs
const STATIC_ASSET_EXTENSIONS = ['.zip', '.txt', '.pdf', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico'];
@ -108,11 +108,27 @@ function extractAnchors(content) {
* /docs/how-to/installation/install-bmad.md -> docs/how-to/installation/install-bmad.md
* /how-to/installation/install-bmad/ -> docs/how-to/installation/install-bmad.md or .../index.md
*/
function resolveLink(siteRelativePath) {
function resolveLink(siteRelativePath, sourceFile) {
// Strip anchor and query
let checkPath = siteRelativePath.split('#')[0].split('?')[0];
// Strip /docs/ prefix if present (repo-relative links)
// Handle relative paths (including bare .md): resolve from source file's directory
if (checkPath.startsWith('./') || checkPath.startsWith('../') || (!checkPath.startsWith('/') && checkPath.endsWith('.md'))) {
const sourceDir = path.dirname(sourceFile);
const resolved = path.resolve(sourceDir, checkPath);
// Ensure the resolved path stays within DOCS_ROOT
if (!resolved.startsWith(DOCS_ROOT + path.sep) && resolved !== DOCS_ROOT) return null;
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) return resolved;
if (fs.existsSync(resolved + '.md')) return resolved + '.md';
// Directory: check for index.md
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
const indexFile = path.join(resolved, 'index.md');
if (fs.existsSync(indexFile)) return indexFile;
}
return null;
}
// Strip /docs/ prefix if present (legacy absolute links)
if (checkPath.startsWith('/docs/')) {
checkPath = checkPath.slice(5); // Remove '/docs' but keep leading '/'
}
@ -129,12 +145,18 @@ function resolveLink(siteRelativePath) {
// Direct path (e.g., /path/file.md)
const direct = path.join(DOCS_ROOT, checkPath);
if (fs.existsSync(direct)) return direct;
if (fs.existsSync(direct) && fs.statSync(direct).isFile()) return direct;
// Try with .md extension
const withMd = direct + '.md';
if (fs.existsSync(withMd)) return withMd;
// Directory without trailing slash: check for index.md
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
const indexFile = path.join(direct, 'index.md');
if (fs.existsSync(indexFile)) return indexFile;
}
return null;
}
@ -144,7 +166,7 @@ function resolveLink(siteRelativePath) {
function findFileWithContext(brokenPath) {
// Extract filename and parent directory from the broken path
// e.g., /tutorials/getting-started/foo/ -> parent: getting-started, file: foo.md
const cleanPath = brokenPath.replace(/\/$/, '').replace(/^\//, '');
const cleanPath = brokenPath.replace(/\/$/, '').replace(/^(\.\.\/|\.\/|\/)+/, '');
const parts = cleanPath.split('/');
const fileName = parts.at(-1) + '.md';
const parentDir = parts.length > 1 ? parts.at(-2) : null;
@ -219,7 +241,7 @@ function processFile(filePath) {
}
// Validate the link target exists
const targetFile = resolveLink(linkPath);
const targetFile = resolveLink(linkPath, filePath);
if (!targetFile) {
// Link is broken - try to find the file

View File

@ -29,6 +29,7 @@
const fs = require('node:fs');
const path = require('node:path');
const yaml = require('yaml');
const { parse: parseCsv } = require('csv-parse/sync');
const PROJECT_ROOT = path.resolve(__dirname, '..');
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
@ -38,10 +39,10 @@ const STRICT = process.argv.includes('--strict');
// --- Constants ---
// File extensions to scan
const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml']);
const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml', '.csv']);
// Skip directories
const SKIP_DIRS = new Set(['node_modules', '_module-installer', '.git']);
const SKIP_DIRS = new Set(['node_modules', '.git']);
// Pattern: {project-root}/_bmad/ references
const PROJECT_ROOT_REF = /\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/g;
@ -292,6 +293,46 @@ function extractMarkdownRefs(filePath, content) {
return refs;
}
function extractCsvRefs(filePath, content) {
const refs = [];
let records;
try {
records = parseCsv(content, {
columns: true,
skip_empty_lines: true,
relax_column_count: true,
});
} catch (error) {
// No CSV schema validator exists yet (planned as Layer 2c) — surface parse errors visibly.
// YAML equivalent (line ~198) defers to validate-agent-schema.js; CSV has no such fallback.
const rel = path.relative(PROJECT_ROOT, filePath);
console.error(` [CSV-PARSE-ERROR] ${rel}: ${error.message}`);
if (process.env.GITHUB_ACTIONS) {
console.log(`::warning file=${rel},line=1::${escapeAnnotation(`CSV parse error: ${error.message}`)}`);
}
return refs;
}
// Only process if workflow-file column exists
const firstRecord = records[0];
if (!firstRecord || !('workflow-file' in firstRecord)) {
return refs;
}
for (const [i, record] of records.entries()) {
const raw = record['workflow-file'];
if (!raw || raw.trim() === '') continue;
if (!isResolvable(raw)) continue;
// Line = header (1) + data row index (0-based) + 1
const line = i + 2;
refs.push({ file: filePath, raw, type: 'project-root', line });
}
return refs;
}
// --- Reference Resolution ---
function resolveRef(ref) {
@ -351,130 +392,163 @@ function checkAbsolutePathLeaks(filePath, content) {
return leaks;
}
// --- Exports (for testing) ---
module.exports = { extractCsvRefs };
// --- Main ---
console.log(`\nValidating file references in: ${SRC_DIR}`);
console.log(`Mode: ${STRICT ? 'STRICT (exit 1 on issues)' : 'WARNING (exit 0)'}${VERBOSE ? ' + VERBOSE' : ''}\n`);
if (require.main === module) {
console.log(`\nValidating file references in: ${SRC_DIR}`);
console.log(`Mode: ${STRICT ? 'STRICT (exit 1 on issues)' : 'WARNING (exit 0)'}${VERBOSE ? ' + VERBOSE' : ''}\n`);
const files = getSourceFiles(SRC_DIR);
console.log(`Found ${files.length} source files\n`);
const files = getSourceFiles(SRC_DIR);
console.log(`Found ${files.length} source files\n`);
let totalRefs = 0;
let brokenRefs = 0;
let totalLeaks = 0;
let filesWithIssues = 0;
const allIssues = []; // Collect for $GITHUB_STEP_SUMMARY
let totalRefs = 0;
let brokenRefs = 0;
let totalLeaks = 0;
let filesWithIssues = 0;
const allIssues = []; // Collect for $GITHUB_STEP_SUMMARY
for (const filePath of files) {
const relativePath = path.relative(PROJECT_ROOT, filePath);
const content = fs.readFileSync(filePath, 'utf-8');
const ext = path.extname(filePath);
for (const filePath of files) {
const relativePath = path.relative(PROJECT_ROOT, filePath);
const content = fs.readFileSync(filePath, 'utf-8');
const ext = path.extname(filePath);
// Extract references
let refs;
if (ext === '.yaml' || ext === '.yml') {
refs = extractYamlRefs(filePath, content);
} else {
refs = extractMarkdownRefs(filePath, content);
}
// Extract references
let refs;
if (ext === '.yaml' || ext === '.yml') {
refs = extractYamlRefs(filePath, content);
} else if (ext === '.csv') {
refs = extractCsvRefs(filePath, content);
} else {
refs = extractMarkdownRefs(filePath, content);
}
// Resolve and check
const broken = [];
// Resolve and classify all refs before printing anything.
// This avoids the confusing pattern of printing headers at two different
// times depending on verbosity — collect first, then print once.
const broken = [];
const ok = [];
if (VERBOSE && refs.length > 0) {
console.log(`\n${relativePath}`);
}
for (const ref of refs) {
totalRefs++;
const resolved = resolveRef(ref);
for (const ref of refs) {
totalRefs++;
const resolved = resolveRef(ref);
if (resolved && !fs.existsSync(resolved)) {
// For paths without extensions, also check if it's a directory
const hasExt = path.extname(resolved) !== '';
if (!hasExt) {
// Could be a directory reference — skip if not clearly a file
if (resolved && !fs.existsSync(resolved)) {
// Extensionless paths may be directory references or partial templates.
// If the path has no extension, check whether it exists as a directory.
// Flag it if nothing exists at all — likely a real broken reference.
const hasExt = path.extname(resolved) !== '';
if (!hasExt) {
if (fs.existsSync(resolved)) {
ok.push({ ref, tag: 'OK-DIR' });
} else {
// No extension and nothing exists — not a file, not a directory.
// Flag as UNRESOLVED (distinct from BROKEN which means "file with extension not found").
broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved), kind: 'unresolved' });
brokenRefs++;
}
continue;
}
broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved), kind: 'broken' });
brokenRefs++;
continue;
}
broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved) });
brokenRefs++;
continue;
if (resolved) {
ok.push({ ref, tag: 'OK' });
}
}
if (VERBOSE && resolved) {
console.log(` [OK] ${ref.raw}`);
}
}
// Check absolute path leaks
const leaks = checkAbsolutePathLeaks(filePath, content);
totalLeaks += leaks.length;
// Check absolute path leaks
const leaks = checkAbsolutePathLeaks(filePath, content);
totalLeaks += leaks.length;
// Print results — file header appears once, in one place
const hasFileIssues = broken.length > 0 || leaks.length > 0;
// Report issues for this file
if (broken.length > 0 || leaks.length > 0) {
filesWithIssues++;
if (!VERBOSE) {
if (hasFileIssues) {
filesWithIssues++;
console.log(`\n${relativePath}`);
}
for (const { ref, resolved } of broken) {
const location = ref.line ? `line ${ref.line}` : ref.key ? `key: ${ref.key}` : '';
console.log(` [BROKEN] ${ref.raw}${location ? ` (${location})` : ''}`);
console.log(` Target not found: ${resolved}`);
allIssues.push({ file: relativePath, line: ref.line || 1, ref: ref.raw, issue: 'broken ref' });
if (process.env.GITHUB_ACTIONS) {
const line = ref.line || 1;
console.log(`::warning file=${relativePath},line=${line}::${escapeAnnotation(`Broken reference: ${ref.raw}${resolved}`)}`);
if (VERBOSE) {
for (const { ref, tag, note } of ok) {
const suffix = note ? ` (${note})` : '';
console.log(` [${tag}] ${ref.raw}${suffix}`);
}
}
}
for (const leak of leaks) {
console.log(` [ABS-PATH] Line ${leak.line}: ${leak.content}`);
allIssues.push({ file: relativePath, line: leak.line, ref: leak.content, issue: 'abs-path' });
if (process.env.GITHUB_ACTIONS) {
console.log(`::warning file=${relativePath},line=${leak.line}::${escapeAnnotation(`Absolute path leak: ${leak.content}`)}`);
for (const { ref, resolved, kind } of broken) {
const location = ref.line ? `line ${ref.line}` : ref.key ? `key: ${ref.key}` : '';
const tag = kind === 'unresolved' ? 'UNRESOLVED' : 'BROKEN';
const detail = kind === 'unresolved' ? 'Not found as file or directory' : 'Target not found';
const issueType = kind === 'unresolved' ? 'unresolved path' : 'broken ref';
console.log(` [${tag}] ${ref.raw}${location ? ` (${location})` : ''}`);
console.log(` ${detail}: ${resolved}`);
allIssues.push({ file: relativePath, line: ref.line || 1, ref: ref.raw, issue: issueType });
if (process.env.GITHUB_ACTIONS) {
const line = ref.line || 1;
console.log(
`::warning file=${relativePath},line=${line}::${escapeAnnotation(`${tag === 'UNRESOLVED' ? 'Unresolved path' : 'Broken reference'}: ${ref.raw}${resolved}`)}`,
);
}
}
for (const leak of leaks) {
console.log(` [ABS-PATH] Line ${leak.line}: ${leak.content}`);
allIssues.push({ file: relativePath, line: leak.line, ref: leak.content, issue: 'abs-path' });
if (process.env.GITHUB_ACTIONS) {
console.log(`::warning file=${relativePath},line=${leak.line}::${escapeAnnotation(`Absolute path leak: ${leak.content}`)}`);
}
}
} else if (VERBOSE && refs.length > 0) {
console.log(`\n${relativePath}`);
for (const { ref, tag, note } of ok) {
const suffix = note ? ` (${note})` : '';
console.log(` [${tag}] ${ref.raw}${suffix}`);
}
}
}
}
// Summary
console.log(`\n${'─'.repeat(60)}`);
console.log(`\nSummary:`);
console.log(` Files scanned: ${files.length}`);
console.log(` References checked: ${totalRefs}`);
console.log(` Broken references: ${brokenRefs}`);
console.log(` Absolute path leaks: ${totalLeaks}`);
// Summary
console.log(`\n${'─'.repeat(60)}`);
console.log(`\nSummary:`);
console.log(` Files scanned: ${files.length}`);
console.log(` References checked: ${totalRefs}`);
console.log(` Broken references: ${brokenRefs}`);
console.log(` Absolute path leaks: ${totalLeaks}`);
const hasIssues = brokenRefs > 0 || totalLeaks > 0;
const hasIssues = brokenRefs > 0 || totalLeaks > 0;
if (hasIssues) {
console.log(`\n ${filesWithIssues} file(s) with issues`);
if (hasIssues) {
console.log(`\n ${filesWithIssues} file(s) with issues`);
if (STRICT) {
console.log(`\n [STRICT MODE] Exiting with failure.`);
if (STRICT) {
console.log(`\n [STRICT MODE] Exiting with failure.`);
} else {
console.log(`\n Run with --strict to treat warnings as errors.`);
}
} else {
console.log(`\n Run with --strict to treat warnings as errors.`);
console.log(`\n All file references valid!`);
}
} else {
console.log(`\n All file references valid!`);
}
console.log('');
console.log('');
// Write GitHub Actions step summary
if (process.env.GITHUB_STEP_SUMMARY) {
let summary = '## File Reference Validation\n\n';
if (allIssues.length > 0) {
summary += '| File | Line | Reference | Issue |\n';
summary += '|------|------|-----------|-------|\n';
for (const issue of allIssues) {
summary += `| ${escapeTableCell(issue.file)} | ${issue.line} | ${escapeTableCell(issue.ref)} | ${issue.issue} |\n`;
// Write GitHub Actions step summary
if (process.env.GITHUB_STEP_SUMMARY) {
let summary = '## File Reference Validation\n\n';
if (allIssues.length > 0) {
summary += '| File | Line | Reference | Issue |\n';
summary += '|------|------|-----------|-------|\n';
for (const issue of allIssues) {
summary += `| ${escapeTableCell(issue.file)} | ${issue.line} | ${escapeTableCell(issue.ref)} | ${issue.issue} |\n`;
}
summary += '\n';
}
summary += '\n';
summary += `**${files.length} files scanned, ${totalRefs} references checked, ${brokenRefs + totalLeaks} issues found**\n`;
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
}
summary += `**${files.length} files scanned, ${totalRefs} references checked, ${brokenRefs + totalLeaks} issues found**\n`;
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
}
process.exit(hasIssues && STRICT ? 1 : 0);
process.exit(hasIssues && STRICT ? 1 : 0);
}

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<rect width="100" height="100" rx="20" fill="#0d9488"/>
<text x="50" y="65" font-family="Arial, sans-serif" font-size="40" font-weight="bold" fill="white" text-anchor="middle">B</text>
</svg>

Before

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 MiB

View File

@ -4,17 +4,16 @@ import { getSiteUrl } from '../lib/site-url.mjs';
const llmsFullUrl = `${getSiteUrl()}/llms-full.txt`;
---
<div class="ai-banner">
<div class="ai-banner" role="note" aria-label="AI documentation notice">
<span>🤖 Consolidated, AI-optimized BMAD docs: <a href={llmsFullUrl}>llms-full.txt</a>. Fetch this plain text file for complete context.</span>
</div>
<style>
.ai-banner {
width: 100vw; /* Full viewport width */
margin-left: calc(-50vw + 50%); /* Center and break out of container */
width: 100%;
height: var(--ai-banner-height, 2.75rem);
background: #334155;
color: rgb(148, 163, 184);
color: #cbd5e1;
padding: 0.5rem 1rem;
font-size: 0.875rem;
border-bottom: 1px solid rgba(140, 140, 255, 0.15);
@ -33,13 +32,18 @@ const llmsFullUrl = `${getSiteUrl()}/llms-full.txt`;
max-width: 100%;
}
.ai-banner a {
color: #8C8CFF;
color: #B9B9FF;
text-decoration: none;
font-weight: 600;
}
.ai-banner a:hover {
text-decoration: underline;
}
.ai-banner a:focus-visible {
outline: 2px solid #B9B9FF;
outline-offset: 2px;
border-radius: 2px;
}
/* Match navbar padding at breakpoints */
@media (min-width: 50rem) {

View File

@ -5,6 +5,10 @@
* /img/foo.png /BMAD-METHOD/img/foo.png (when base is /BMAD-METHOD/)
* /llms.txt /BMAD-METHOD/llms.txt
*
* Supported elements:
* - img[src], iframe[src], video[src], source[src], audio[src]
* - a[href], link[href]
*
* Only affects absolute paths (/) - relative paths and external URLs are unchanged.
* Does NOT process .md links (those are handled by rehype-markdown-links).
*/
@ -21,37 +25,57 @@ import { visit } from 'unist-util-visit';
export default function rehypeBasePaths(options = {}) {
const base = options.base || '/';
// Normalize base: ensure it ends with / and doesn't have double slashes
// Normalize base: ensure trailing slash so concatenation with path.slice(1) (no leading /)
// produces correct paths like /BMAD-METHOD/img/foo.png.
// Note: rehype-markdown-links uses the opposite convention (strips trailing slash) because
// it concatenates with paths that start with /.
const normalizedBase = base === '/' ? '/' : base.endsWith('/') ? base : base + '/';
/**
* Prepend base path to an absolute URL attribute if needed.
* Skips protocol-relative URLs (//) and paths that already include the base.
*
* @param {object} node - HAST element node
* @param {string} attr - Attribute name ('src' or 'href')
*/
function prependBase(node, attr) {
const value = node.properties?.[attr];
if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//')) {
return;
}
if (normalizedBase !== '/' && !value.startsWith(normalizedBase)) {
node.properties[attr] = normalizedBase + value.slice(1);
}
}
return (tree) => {
// Handle raw HTML blocks (inline HTML in markdown that isn't parsed into HAST elements)
if (normalizedBase !== '/') {
visit(tree, 'raw', (node) => {
// Replace absolute src="/..." and href="/..." attributes, skipping protocol-relative
// and paths that already have the base prefix
node.value = node.value.replace(/(?<attr>\b(?:src|href))="(?<path>\/(?!\/)[^"]*)"/g, (match, attr, pathValue) => {
if (pathValue.startsWith(normalizedBase)) return match;
return `${attr}="${normalizedBase}${pathValue.slice(1)}"`;
});
});
}
visit(tree, 'element', (node) => {
// Process img tags with src attribute
if (node.tagName === 'img' && node.properties?.src) {
const src = node.properties.src;
const tag = node.tagName;
if (typeof src === 'string' && src.startsWith('/') && !src.startsWith('//')) {
// Don't transform if already has the base path
if (normalizedBase !== '/' && !src.startsWith(normalizedBase)) {
node.properties.src = normalizedBase + src.slice(1);
}
}
// Tags with src attribute
if (['img', 'iframe', 'video', 'source', 'audio'].includes(tag)) {
prependBase(node, 'src');
}
// Process iframe tags with src attribute
if (node.tagName === 'iframe' && node.properties?.src) {
const src = node.properties.src;
if (typeof src === 'string' && src.startsWith('/') && !src.startsWith('//')) {
// Don't transform if already has the base path
if (normalizedBase !== '/' && !src.startsWith(normalizedBase)) {
node.properties.src = normalizedBase + src.slice(1);
}
}
// Link tags with href attribute (stylesheets, preloads, etc.)
if (tag === 'link') {
prependBase(node, 'href');
}
// Process anchor tags with href attribute
if (node.tagName === 'a' && node.properties?.href) {
// Anchor tags need special handling - skip .md links
if (tag === 'a' && node.properties?.href) {
const href = node.properties.href;
if (typeof href !== 'string') {

View File

@ -1,117 +1,111 @@
/**
* Rehype plugin to transform markdown file links (.md) to page routes
* Rehype plugin to transform relative .md links into correct site URLs.
*
* Transforms:
* ./path/to/file.md ./path/to/file/
* ./path/index.md ./path/ (index.md becomes directory root)
* ../path/file.md#anchor ../path/file/#anchor
* ./file.md?query=param ./file/?query=param
* /docs/absolute/path/file.md {base}/absolute/path/file/
* Uses the source file's disk path (via vfile) to resolve the link target,
* then computes the output URL relative to the content root directory.
* This correctly handles Starlight's directory-per-page URL structure
* where ./sibling.md from reference/testing.md must become /reference/sibling/
* (not ./sibling/ which would resolve to /reference/testing/sibling/).
*
* For absolute paths starting with /docs/, the /docs prefix is stripped
* since the Astro site serves content from the docs directory as the root.
* The base path is prepended to absolute paths for subdirectory deployments.
*
* Affects relative links (./, ../) and absolute paths (/) - external links are unchanged
* Supports: ./sibling.md, ../other/page.md, bare.md, /docs/absolute.md
* Preserves: query strings, hash anchors
* Skips: external URLs, non-.md links
*/
import { visit } from 'unist-util-visit';
import path from 'node:path';
/**
* Convert Markdown file links (.md) into equivalent page route-style links.
*
* The returned transformer walks the HTML tree and rewrites anchor `href` values that are relative paths (./, ../) or absolute paths (/) pointing to `.md` files. It preserves query strings and hash anchors, rewrites `.../index.md` to the directory root path (`.../`), and rewrites other `.md` file paths by removing the `.md` extension and ensuring a trailing slash. External links (http://, https://) and non-.md links are left unchanged.
*
* @param {Object} options - Plugin options
* @param {string} options.base - The base path to prepend to absolute URLs (e.g., '/BMAD-METHOD/')
* @returns {function} A HAST tree transformer that mutates `a` element `href` properties as described.
* @param {Object} options
* @param {string} options.base - Site base path (e.g., '/BMAD-METHOD/')
* @param {string} [options.contentDir] - Absolute path to content root; auto-detected if omitted
*/
export default function rehypeMarkdownLinks(options = {}) {
const base = options.base || '/';
// Normalize base: ensure it ends with / and doesn't have double slashes
const normalizedBase = base === '/' ? '' : base.endsWith('/') ? base.slice(0, -1) : base;
const normalizedBase = base === '/' ? '' : base.replace(/\/$/, '');
return (tree, file) => {
// The current file's absolute path on disk, set by Astro's markdown pipeline
const currentFilePath = file.path;
if (!currentFilePath) return;
// Auto-detect content root: walk up from current file to find src/content/docs
const contentDir = options.contentDir || detectContentDir(currentFilePath);
if (!contentDir) {
throw new Error(`[rehype-markdown-links] Could not detect content directory for: ${currentFilePath}`);
}
return (tree) => {
visit(tree, 'element', (node) => {
// Only process anchor tags with href
if (node.tagName !== 'a' || !node.properties?.href) {
if (node.tagName !== 'a' || typeof node.properties?.href !== 'string') {
return;
}
const href = node.properties.href;
// Skip if not a string (shouldn't happen, but be safe)
if (typeof href !== 'string') {
// Skip external links (including protocol-relative URLs like //cdn.example.com)
if (href.includes('://') || href.startsWith('//') || href.startsWith('mailto:') || href.startsWith('tel:')) {
return;
}
// Skip external links (http://, https://, mailto:, etc.)
if (href.includes('://') || href.startsWith('mailto:') || href.startsWith('tel:')) {
return;
}
// Split href into path vs query+fragment suffix
const delimIdx = findFirstDelimiter(href);
const linkPath = delimIdx === -1 ? href : href.substring(0, delimIdx);
const suffix = delimIdx === -1 ? '' : href.substring(delimIdx);
// Only transform paths starting with ./, ../, or / (absolute)
if (!href.startsWith('./') && !href.startsWith('../') && !href.startsWith('/')) {
return;
}
// Only process .md links
if (!linkPath.endsWith('.md')) return;
// Extract path portion (before ? and #) to check if it's a .md file
const firstDelimiter = Math.min(
href.indexOf('?') === -1 ? Infinity : href.indexOf('?'),
href.indexOf('#') === -1 ? Infinity : href.indexOf('#'),
);
const pathPortion = firstDelimiter === Infinity ? href : href.substring(0, firstDelimiter);
// Don't transform if path doesn't end with .md
if (!pathPortion.endsWith('.md')) {
return;
}
// Split the URL into parts: path, anchor, and query
let urlPath = pathPortion;
let anchor = '';
let query = '';
// Extract query string and anchor from original href
if (firstDelimiter !== Infinity) {
const suffix = href.substring(firstDelimiter);
const anchorInSuffix = suffix.indexOf('#');
if (suffix.startsWith('?')) {
if (anchorInSuffix !== -1) {
query = suffix.substring(0, anchorInSuffix);
anchor = suffix.substring(anchorInSuffix);
} else {
query = suffix;
}
} else {
// starts with #
anchor = suffix;
}
}
// Track if this was an absolute path (for base path prepending)
const isAbsolute = urlPath.startsWith('/');
// Strip /docs/ prefix from absolute paths (repo-relative → site-relative)
if (urlPath.startsWith('/docs/')) {
urlPath = urlPath.slice(5); // Remove '/docs' prefix, keeping the leading /
}
// Transform .md to /
// Special case: index.md → directory root (e.g., ./tutorials/index.md → ./tutorials/)
if (urlPath.endsWith('/index.md')) {
urlPath = urlPath.replace(/\/index\.md$/, '/');
// Resolve the target file's absolute path on disk
let targetPath;
if (linkPath.startsWith('/docs/')) {
// Absolute /docs/ path — resolve from content root
targetPath = path.join(contentDir, linkPath.slice(5)); // strip '/docs'
} else if (linkPath.startsWith('/')) {
// Other absolute paths — resolve from content root
targetPath = path.join(contentDir, linkPath);
} else {
urlPath = urlPath.replace(/\.md$/, '/');
// Relative path (./sibling.md, ../other.md, bare.md) — resolve from current file
targetPath = path.resolve(path.dirname(currentFilePath), linkPath);
}
// Prepend base path to absolute URLs for subdirectory deployments
if (isAbsolute && normalizedBase) {
urlPath = normalizedBase + urlPath;
// Compute the target's path relative to content root
const relativeToContent = path.relative(contentDir, targetPath);
// Safety: skip if target resolves outside content root
if (relativeToContent.startsWith('..')) return;
// Convert file path to URL: strip .md, handle index, ensure leading/trailing slashes
let urlPath = relativeToContent.replace(/\.md$/, '');
// index.md becomes the directory root
if (urlPath.endsWith('/index') || urlPath === 'index') {
urlPath = urlPath.slice(0, -'index'.length);
}
// Reconstruct the href
node.properties.href = urlPath + query + anchor;
// Build absolute URL with base path, normalizing any double slashes
const raw = normalizedBase + '/' + urlPath.replace(/\/?$/, '/') + suffix;
node.properties.href = raw.replace(/\/\/+/g, '/');
});
};
}
/** Find the index of the first ? or # in a string, or -1 if neither exists. */
export function findFirstDelimiter(str) {
const q = str.indexOf('?');
const h = str.indexOf('#');
if (q === -1) return h;
if (h === -1) return q;
return Math.min(q, h);
}
/** Walk up from a file path to find the content docs directory. */
export function detectContentDir(filePath) {
const segments = filePath.split(path.sep);
// Look for src/content/docs in the path
for (let i = segments.length - 1; i >= 2; i--) {
if (segments[i - 2] === 'src' && segments[i - 1] === 'content' && segments[i] === 'docs') {
return segments.slice(0, i + 1).join(path.sep);
}
}
return null;
}

View File

@ -17,12 +17,12 @@
--sl-nav-height: 6.25rem; /* Base nav height (~3.5rem) + banner height (2.75rem) */
/* Full-width content - override Starlight's default 45rem/67.5rem */
--sl-content-width: 100%;
--sl-content-width: 65rem;
/* Primary accent colors - purple to match Docusaurus */
--sl-color-accent-low: #e0e0ff;
--sl-color-accent: #8C8CFF;
--sl-color-accent-high: #4141FF;
--sl-color-accent: #5E5ED0;
--sl-color-accent-high: #3333CC;
/* Text colors */
--sl-color-white: #1e293b;
@ -41,7 +41,7 @@
--sl-line-height: 1.7;
/* Code highlighting */
--sl-color-bg-inline-code: rgba(140, 140, 255, 0.1);
--sl-color-bg-inline-code: rgba(94, 94, 208, 0.1);
}
/* ============================================
@ -49,7 +49,7 @@
============================================ */
:root[data-theme='dark'] {
/* Full-width content - override Starlight's default */
--sl-content-width: 100%;
--sl-content-width: 65rem;
/* Primary accent colors - purple to match Docusaurus */
--sl-color-accent-low: #2a2a5a;
@ -138,7 +138,7 @@
/* Active state - thin left accent bar */
.sidebar-content a[aria-current='page'] {
background-color: rgba(140, 140, 255, 0.08);
background-color: rgba(94, 94, 208, 0.08);
color: var(--sl-color-accent);
border-left-color: var(--sl-color-accent);
font-weight: 600;
@ -281,7 +281,7 @@ header.header .header.sl-flex {
.card:hover {
transform: translateY(-3px);
border-color: var(--sl-color-accent);
box-shadow: 0 8px 24px rgba(140, 140, 255, 0.15);
box-shadow: 0 8px 24px rgba(94, 94, 208, 0.15);
}
:root[data-theme='dark'] .card {
@ -339,9 +339,24 @@ button:hover {
MISC ENHANCEMENTS
============================================ */
/* Smooth scrolling */
/* Prevent horizontal scrollbar from full-viewport-width breakout elements (e.g. iframe) */
html {
scroll-behavior: smooth;
overflow-x: clip;
}
/* Smooth scrolling */
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
/* Disable hover animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
.card:hover,
.sl-link-card:hover {
transform: none;
}
}
/* Better link underlines */
@ -371,7 +386,7 @@ table {
/* Blockquotes */
blockquote {
border-left-color: var(--sl-color-accent);
background-color: rgba(140, 140, 255, 0.05);
background-color: rgba(94, 94, 208, 0.05);
border-radius: 0 8px 8px 0;
padding: 1rem 1.25rem;
}
@ -395,7 +410,7 @@ blockquote {
}
.starlight-aside--tip .starlight-aside__title {
color: #059669;
color: #047857;
}
:root[data-theme='dark'] .starlight-aside--tip {
@ -408,11 +423,11 @@ blockquote {
/* Note aside */
.starlight-aside--note {
background-color: rgba(140, 140, 255, 0.08);
background-color: rgba(94, 94, 208, 0.08);
}
.starlight-aside--note .starlight-aside__title {
color: #8C8CFF;
color: #5C5CCC;
}
:root[data-theme='dark'] .starlight-aside--note {
@ -429,7 +444,7 @@ blockquote {
}
.starlight-aside--caution .starlight-aside__title {
color: #d97706;
color: #a14908;
}
:root[data-theme='dark'] .starlight-aside--caution {
@ -446,7 +461,7 @@ blockquote {
}
.starlight-aside--danger .starlight-aside__title {
color: #dc2626;
color: #be1c1c;
}
:root[data-theme='dark'] .starlight-aside--danger {
@ -464,16 +479,10 @@ blockquote {
}
/* ============================================
FOOTER - Minimal styling
FOOTER - No custom styling needed
The only <footer> in Starlight is the content footer
(meta/pagination), which should stay transparent.
============================================ */
footer {
background-color: var(--sl-color-black);
border-top: 1px solid var(--sl-color-hairline);
}
:root[data-theme='dark'] footer {
background-color: #020617;
}
/* ============================================
RESPONSIVE ADJUSTMENTS