Merge branch 'main' of https://github.com/pi-docket/BMAD-METHOD
This commit is contained in:
commit
892db62df2
|
|
@ -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"
|
||||
Binary file not shown.
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
name: Trigger CodeRabbit on Ready for Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [ready_for_review]
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
53
CHANGELOG.md
53
CHANGELOG.md
|
|
@ -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**
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
---
|
||||
title: "BMGD Quick Guide"
|
||||
description: Quick reference for BMad Game Dev Studio
|
||||
draft: true
|
||||
---
|
||||
|
||||

|
||||
|
||||
# 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.
|
||||
|
||||

|
||||
|
||||
## 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)
|
||||
|
|
@ -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 |
|
|
@ -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.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
title: "Brainstorming"
|
||||
description: Interactive creative sessions using 60+ proven ideation techniques
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
Unlock your creativity through guided exploration.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>.
|
||||
:::
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module,phase,name,workflow-file,description
|
||||
bmm,anytime,Document,,Analyze project
|
||||
bmm,1-analysis,Brainstorm,,Brainstorm ideas
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module,phase,name,workflow-file,description
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
name,code,description,agent
|
||||
brainstorm,BSP,"Generate ideas",analyst
|
||||
party,PM,"Multi-agent",facilitator
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
name,workflow-file,description
|
||||
test,_bmad/core/tasks/help.md,A test entry
|
||||
|
|
|
@ -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);
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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.
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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!
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue