chore: standardize ESLint/Prettier formatting across codebase

This commit is contained in:
manjaroblack 2025-08-15 22:22:24 -05:00
parent e1176f337e
commit 74c78d2274
No known key found for this signature in database
GPG Key ID: 02FD4111DA5560B4
113 changed files with 5397 additions and 3494 deletions

View File

@ -1,9 +1,9 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: "" title: ''
labels: "" labels: ''
assignees: "" assignees: ''
--- ---
**Describe the bug** **Describe the bug**

View File

@ -1,9 +1,9 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: "" title: ''
labels: "" labels: ''
assignees: "" assignees: ''
--- ---
**Did you discuss the idea first in Discord Server (#general-dev)** **Did you discuss the idea first in Discord Server (#general-dev)**

View File

@ -1,6 +1,15 @@
name: Discord Notification name: Discord Notification
on: [pull_request, release, create, delete, issue_comment, pull_request_review, pull_request_review_comment] on:
[
pull_request,
release,
create,
delete,
issue_comment,
pull_request_review,
pull_request_review_comment,
]
jobs: jobs:
notify: notify:

42
.github/workflows/format-check.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: format-check
on:
pull_request:
branches: ["**"]
jobs:
prettier:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Prettier format check
run: npm run format:check
eslint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: ESLint
run: npm run lint

View File

@ -4,9 +4,9 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version_bump: version_bump:
description: 'Version bump type' description: "Version bump type"
required: true required: true
default: 'minor' default: "minor"
type: choice type: choice
options: options:
- patch - patch
@ -30,8 +30,8 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: "20"
registry-url: 'https://registry.npmjs.org' registry-url: "https://registry.npmjs.org"
- name: Configure Git - name: Configure Git
run: | run: |
@ -83,27 +83,6 @@ jobs:
;; ;;
esac esac
# Check if calculated version already exists on NPM and increment if necessary
while npm view bmad-method@$NEW_VERSION version >/dev/null 2>&1; do
echo "Version $NEW_VERSION already exists, incrementing..."
IFS='.' read -ra NEW_VERSION_PARTS <<< "$NEW_VERSION"
NEW_MAJOR=${NEW_VERSION_PARTS[0]}
NEW_MINOR=${NEW_VERSION_PARTS[1]}
NEW_PATCH=${NEW_VERSION_PARTS[2]}
case "${{ github.event.inputs.version_bump }}" in
"major")
NEW_VERSION="$((NEW_MAJOR + 1)).0.0"
;;
"minor")
NEW_VERSION="$NEW_MAJOR.$((NEW_MINOR + 1)).0"
;;
"patch")
NEW_VERSION="$NEW_MAJOR.$NEW_MINOR.$((NEW_PATCH + 1))"
;;
esac
done
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "Promoting from $CURRENT_VERSION to $NEW_VERSION" echo "Promoting from $CURRENT_VERSION to $NEW_VERSION"
@ -121,10 +100,9 @@ jobs:
- name: Commit stable release - name: Commit stable release
run: | run: |
git add . git add .
git commit -m "feat: promote to stable ${{ steps.version.outputs.new_version }} git commit -m "release: promote to stable ${{ steps.version.outputs.new_version }}
BREAKING CHANGE: Promote beta features to stable release
- Promote beta features to stable release
- Update version from ${{ steps.version.outputs.current_version }} to ${{ steps.version.outputs.new_version }} - Update version from ${{ steps.version.outputs.current_version }} to ${{ steps.version.outputs.new_version }}
- Automated promotion via GitHub Actions" - Automated promotion via GitHub Actions"

View File

@ -1,5 +1,5 @@
name: Release name: Release
'on': "on":
push: push:
branches: branches:
- main - main
@ -23,7 +23,7 @@ permissions:
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: '!contains(github.event.head_commit.message, ''[skip ci]'')' if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -33,7 +33,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: "20"
cache: npm cache: npm
registry-url: https://registry.npmjs.org registry-url: https://registry.npmjs.org
- name: Install dependencies - name: Install dependencies

1
.gitignore vendored
View File

@ -25,7 +25,6 @@ Thumbs.db
# Development tools and configs # Development tools and configs
.prettierignore .prettierignore
.prettierrc .prettierrc
.husky/
# IDE and editor configs # IDE and editor configs
.windsurf/ .windsurf/

3
.husky/pre-commit Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env sh
npx --no-install lint-staged

27
.vscode/settings.json vendored
View File

@ -40,5 +40,30 @@
"tileset", "tileset",
"Trae", "Trae",
"VNET" "VNET"
] ],
"json.schemas": [
{
"fileMatch": ["package.json"],
"url": "https://json.schemastore.org/package.json"
},
{
"fileMatch": [".vscode/settings.json"],
"url": "vscode://schemas/settings/folder"
}
],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"prettier.prettierPath": "node_modules/prettier",
"prettier.requireConfig": true,
"yaml.format.enable": false,
"eslint.useFlatConfig": true,
"eslint.validate": ["javascript", "yaml"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.rulers": [100]
} }

View File

@ -4,7 +4,7 @@ bundle:
description: Includes every core system agent. description: Includes every core system agent.
agents: agents:
- bmad-orchestrator - bmad-orchestrator
- '*' - "*"
workflows: workflows:
- brownfield-fullstack.yaml - brownfield-fullstack.yaml
- brownfield-service.yaml - brownfield-service.yaml

View File

@ -131,7 +131,7 @@ workflow-guidance:
- Understand each workflow's purpose, options, and decision points - Understand each workflow's purpose, options, and decision points
- Ask clarifying questions based on the workflow's structure - Ask clarifying questions based on the workflow's structure
- Guide users through workflow selection when multiple options exist - Guide users through workflow selection when multiple options exist
- When appropriate, suggest: "Would you like me to create a detailed workflow plan before starting?" - When appropriate, suggest: 'Would you like me to create a detailed workflow plan before starting?'
- For workflows with divergent paths, help users choose the right path - For workflows with divergent paths, help users choose the right path
- Adapt questions to the specific domain (e.g., game dev vs infrastructure vs web dev) - Adapt questions to the specific domain (e.g., game dev vs infrastructure vs web dev)
- Only recommend workflows that actually exist in the current bundle - Only recommend workflows that actually exist in the current bundle

View File

@ -35,7 +35,7 @@ agent:
id: dev id: dev
title: Full Stack Developer title: Full Stack Developer
icon: 💻 icon: 💻
whenToUse: "Use for code implementation, debugging, refactoring, and development best practices" whenToUse: 'Use for code implementation, debugging, refactoring, and development best practices'
customization: customization:
persona: persona:
@ -57,13 +57,13 @@ commands:
- explain: teach me what and why you did whatever you just did in detail so I can learn. Explain to me as if you were training a junior engineer. - explain: teach me what and why you did whatever you just did in detail so I can learn. Explain to me as if you were training a junior engineer.
- exit: Say goodbye as the Developer, and then abandon inhabiting this persona - exit: Say goodbye as the Developer, and then abandon inhabiting this persona
- develop-story: - develop-story:
- order-of-execution: "Read (first or next) task→Implement Task and its subtasks→Write tests→Execute validations→Only if ALL pass, then update the task checkbox with [x]→Update story section File List to ensure it lists and new or modified or deleted source file→repeat order-of-execution until complete" - order-of-execution: 'Read (first or next) task→Implement Task and its subtasks→Write tests→Execute validations→Only if ALL pass, then update the task checkbox with [x]→Update story section File List to ensure it lists and new or modified or deleted source file→repeat order-of-execution until complete'
- story-file-updates-ONLY: - story-file-updates-ONLY:
- CRITICAL: ONLY UPDATE THE STORY FILE WITH UPDATES TO SECTIONS INDICATED BELOW. DO NOT MODIFY ANY OTHER SECTIONS. - CRITICAL: ONLY UPDATE THE STORY FILE WITH UPDATES TO SECTIONS INDICATED BELOW. DO NOT MODIFY ANY OTHER SECTIONS.
- CRITICAL: You are ONLY authorized to edit these specific sections of story files - Tasks / Subtasks Checkboxes, Dev Agent Record section and all its subsections, Agent Model Used, Debug Log References, Completion Notes List, File List, Change Log, Status - CRITICAL: You are ONLY authorized to edit these specific sections of story files - Tasks / Subtasks Checkboxes, Dev Agent Record section and all its subsections, Agent Model Used, Debug Log References, Completion Notes List, File List, Change Log, Status
- CRITICAL: DO NOT modify Status, Story, Acceptance Criteria, Dev Notes, Testing sections, or any other sections not listed above - CRITICAL: DO NOT modify Status, Story, Acceptance Criteria, Dev Notes, Testing sections, or any other sections not listed above
- blocking: "HALT for: Unapproved deps needed, confirm with user | Ambiguous after story check | 3 failures attempting to implement or fix something repeatedly | Missing config | Failing regression" - blocking: 'HALT for: Unapproved deps needed, confirm with user | Ambiguous after story check | 3 failures attempting to implement or fix something repeatedly | Missing config | Failing regression'
- ready-for-review: "Code matches requirements + All validations pass + Follows standards + File List complete" - ready-for-review: 'Code matches requirements + All validations pass + Follows standards + File List complete'
- completion: "All Tasks and Subtasks marked [x] and have tests→Validations and full regression passes (DON'T BE LAZY, EXECUTE ALL TESTS and CONFIRM)→Ensure File List is Complete→run the task execute-checklist for the checklist story-dod-checklist→set story status: 'Ready for Review'→HALT" - completion: "All Tasks and Subtasks marked [x] and have tests→Validations and full regression passes (DON'T BE LAZY, EXECUTE ALL TESTS and CONFIRM)→Ensure File List is Complete→run the task execute-checklist for the checklist story-dod-checklist→set story status: 'Ready for Review'→HALT"
dependencies: dependencies:

View File

@ -298,7 +298,7 @@ You are the "Vibe CEO" - thinking like a CEO with unlimited resources and a sing
- **Claude Code**: `/agent-name` (e.g., `/bmad-master`) - **Claude Code**: `/agent-name` (e.g., `/bmad-master`)
- **Cursor**: `@agent-name` (e.g., `@bmad-master`) - **Cursor**: `@agent-name` (e.g., `@bmad-master`)
- **Windsurf**: `@agent-name` (e.g., `@bmad-master`) - **Windsurf**: `/agent-name` (e.g., `/bmad-master`)
- **Trae**: `@agent-name` (e.g., `@bmad-master`) - **Trae**: `@agent-name` (e.g., `@bmad-master`)
- **Roo Code**: Select mode from mode selector (e.g., `bmad-master`) - **Roo Code**: Select mode from mode selector (e.g., `bmad-master`)
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector.

View File

@ -25,10 +25,10 @@ Comprehensive guide for determining appropriate test levels (unit, integration,
```yaml ```yaml
unit_test: unit_test:
component: "PriceCalculator" component: 'PriceCalculator'
scenario: "Calculate discount with multiple rules" scenario: 'Calculate discount with multiple rules'
justification: "Complex business logic with multiple branches" justification: 'Complex business logic with multiple branches'
mock_requirements: "None - pure function" mock_requirements: 'None - pure function'
``` ```
### Integration Tests ### Integration Tests
@ -52,10 +52,10 @@ unit_test:
```yaml ```yaml
integration_test: integration_test:
components: ["UserService", "AuthRepository"] components: ['UserService', 'AuthRepository']
scenario: "Create user with role assignment" scenario: 'Create user with role assignment'
justification: "Critical data flow between service and persistence" justification: 'Critical data flow between service and persistence'
test_environment: "In-memory database" test_environment: 'In-memory database'
``` ```
### End-to-End Tests ### End-to-End Tests
@ -79,10 +79,10 @@ integration_test:
```yaml ```yaml
e2e_test: e2e_test:
journey: "Complete checkout process" journey: 'Complete checkout process'
scenario: "User purchases with saved payment method" scenario: 'User purchases with saved payment method'
justification: "Revenue-critical path requiring full validation" justification: 'Revenue-critical path requiring full validation'
environment: "Staging with test payment gateway" environment: 'Staging with test payment gateway'
``` ```
## Test Level Selection Rules ## Test Level Selection Rules

View File

@ -1,6 +1,6 @@
--- ---
docOutputLocation: docs/brainstorming-session-results.md docOutputLocation: docs/brainstorming-session-results.md
template: "{root}/templates/brainstorming-output-tmpl.yaml" template: '{root}/templates/brainstorming-output-tmpl.yaml'
--- ---
# Facilitate Brainstorming Session Task # Facilitate Brainstorming Session Task

View File

@ -6,18 +6,19 @@ Quick NFR validation focused on the core four: security, performance, reliabilit
```yaml ```yaml
required: required:
- story_id: "{epic}.{story}" # e.g., "1.3" - story_id: '{epic}.{story}' # e.g., "1.3"
- story_path: "docs/stories/{epic}.{story}.*.md" - story_path: 'docs/stories/{epic}.{story}.*.md'
optional: optional:
- architecture_refs: "docs/architecture/*.md" - architecture_refs: 'docs/architecture/*.md'
- technical_preferences: "docs/technical-preferences.md" - technical_preferences: 'docs/technical-preferences.md'
- acceptance_criteria: From story file - acceptance_criteria: From story file
``` ```
## Purpose ## Purpose
Assess non-functional requirements for a story and generate: Assess non-functional requirements for a story and generate:
1. YAML block for the gate file's `nfr_validation` section 1. YAML block for the gate file's `nfr_validation` section
2. Brief markdown assessment saved to `docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md` 2. Brief markdown assessment saved to `docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md`
@ -26,6 +27,7 @@ Assess non-functional requirements for a story and generate:
### 0. Fail-safe for Missing Inputs ### 0. Fail-safe for Missing Inputs
If story_path or story file can't be found: If story_path or story file can't be found:
- Still create assessment file with note: "Source story not found" - Still create assessment file with note: "Source story not found"
- Set all selected NFRs to CONCERNS with notes: "Target unknown / evidence missing" - Set all selected NFRs to CONCERNS with notes: "Target unknown / evidence missing"
- Continue with assessment to provide value - Continue with assessment to provide value
@ -52,6 +54,7 @@ Which NFRs should I assess? (Enter numbers or press Enter for default)
### 2. Check for Thresholds ### 2. Check for Thresholds
Look for NFR requirements in: Look for NFR requirements in:
- Story acceptance criteria - Story acceptance criteria
- `docs/architecture/*.md` files - `docs/architecture/*.md` files
- `docs/technical-preferences.md` - `docs/technical-preferences.md`
@ -72,6 +75,7 @@ No security requirements found. Required auth method?
### 3. Quick Assessment ### 3. Quick Assessment
For each selected NFR, check: For each selected NFR, check:
- Is there evidence it's implemented? - Is there evidence it's implemented?
- Can we validate it? - Can we validate it?
- Are there obvious gaps? - Are there obvious gaps?
@ -88,16 +92,16 @@ nfr_validation:
_assessed: [security, performance, reliability, maintainability] _assessed: [security, performance, reliability, maintainability]
security: security:
status: CONCERNS status: CONCERNS
notes: "No rate limiting on auth endpoints" notes: 'No rate limiting on auth endpoints'
performance: performance:
status: PASS status: PASS
notes: "Response times < 200ms verified" notes: 'Response times < 200ms verified'
reliability: reliability:
status: PASS status: PASS
notes: "Error handling and retries implemented" notes: 'Error handling and retries implemented'
maintainability: maintainability:
status: CONCERNS status: CONCERNS
notes: "Test coverage at 65%, target is 80%" notes: 'Test coverage at 65%, target is 80%'
``` ```
## Deterministic Status Rules ## Deterministic Status Rules
@ -123,18 +127,21 @@ If `technical-preferences.md` defines custom weights, use those instead.
```markdown ```markdown
# NFR Assessment: {epic}.{story} # NFR Assessment: {epic}.{story}
Date: {date} Date: {date}
Reviewer: Quinn Reviewer: Quinn
<!-- Note: Source story not found (if applicable) --> <!-- Note: Source story not found (if applicable) -->
## Summary ## Summary
- Security: CONCERNS - Missing rate limiting - Security: CONCERNS - Missing rate limiting
- Performance: PASS - Meets <200ms requirement - Performance: PASS - Meets <200ms requirement
- Reliability: PASS - Proper error handling - Reliability: PASS - Proper error handling
- Maintainability: CONCERNS - Test coverage below target - Maintainability: CONCERNS - Test coverage below target
## Critical Issues ## Critical Issues
1. **No rate limiting** (Security) 1. **No rate limiting** (Security)
- Risk: Brute force attacks possible - Risk: Brute force attacks possible
- Fix: Add rate limiting middleware to auth endpoints - Fix: Add rate limiting middleware to auth endpoints
@ -144,6 +151,7 @@ Reviewer: Quinn
- Fix: Add tests for uncovered branches - Fix: Add tests for uncovered branches
## Quick Wins ## Quick Wins
- Add rate limiting: ~2 hours - Add rate limiting: ~2 hours
- Increase test coverage: ~4 hours - Increase test coverage: ~4 hours
- Add performance monitoring: ~1 hour - Add performance monitoring: ~1 hour
@ -152,6 +160,7 @@ Reviewer: Quinn
## Output 3: Story Update Line ## Output 3: Story Update Line
**End with this line for the review task to quote:** **End with this line for the review task to quote:**
``` ```
NFR assessment: docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md NFR assessment: docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md
``` ```
@ -159,6 +168,7 @@ NFR assessment: docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md
## Output 4: Gate Integration Line ## Output 4: Gate Integration Line
**Always print at the end:** **Always print at the end:**
``` ```
Gate NFR block ready → paste into docs/qa/gates/{epic}.{story}-{slug}.yml under nfr_validation Gate NFR block ready → paste into docs/qa/gates/{epic}.{story}-{slug}.yml under nfr_validation
``` ```
@ -166,66 +176,82 @@ Gate NFR block ready → paste into docs/qa/gates/{epic}.{story}-{slug}.yml unde
## Assessment Criteria ## Assessment Criteria
### Security ### Security
**PASS if:** **PASS if:**
- Authentication implemented - Authentication implemented
- Authorization enforced - Authorization enforced
- Input validation present - Input validation present
- No hardcoded secrets - No hardcoded secrets
**CONCERNS if:** **CONCERNS if:**
- Missing rate limiting - Missing rate limiting
- Weak encryption - Weak encryption
- Incomplete authorization - Incomplete authorization
**FAIL if:** **FAIL if:**
- No authentication - No authentication
- Hardcoded credentials - Hardcoded credentials
- SQL injection vulnerabilities - SQL injection vulnerabilities
### Performance ### Performance
**PASS if:** **PASS if:**
- Meets response time targets - Meets response time targets
- No obvious bottlenecks - No obvious bottlenecks
- Reasonable resource usage - Reasonable resource usage
**CONCERNS if:** **CONCERNS if:**
- Close to limits - Close to limits
- Missing indexes - Missing indexes
- No caching strategy - No caching strategy
**FAIL if:** **FAIL if:**
- Exceeds response time limits - Exceeds response time limits
- Memory leaks - Memory leaks
- Unoptimized queries - Unoptimized queries
### Reliability ### Reliability
**PASS if:** **PASS if:**
- Error handling present - Error handling present
- Graceful degradation - Graceful degradation
- Retry logic where needed - Retry logic where needed
**CONCERNS if:** **CONCERNS if:**
- Some error cases unhandled - Some error cases unhandled
- No circuit breakers - No circuit breakers
- Missing health checks - Missing health checks
**FAIL if:** **FAIL if:**
- No error handling - No error handling
- Crashes on errors - Crashes on errors
- No recovery mechanisms - No recovery mechanisms
### Maintainability ### Maintainability
**PASS if:** **PASS if:**
- Test coverage meets target - Test coverage meets target
- Code well-structured - Code well-structured
- Documentation present - Documentation present
**CONCERNS if:** **CONCERNS if:**
- Test coverage below target - Test coverage below target
- Some code duplication - Some code duplication
- Missing documentation - Missing documentation
**FAIL if:** **FAIL if:**
- No tests - No tests
- Highly coupled code - Highly coupled code
- No documentation - No documentation
@ -291,6 +317,7 @@ maintainability:
8. **Portability**: Adaptability, installability 8. **Portability**: Adaptability, installability
Use these when assessing beyond the core four. Use these when assessing beyond the core four.
</details> </details>
<details> <details>
@ -304,12 +331,13 @@ performance_deep_dive:
p99: 350ms p99: 350ms
database: database:
slow_queries: 2 slow_queries: 2
missing_indexes: ["users.email", "orders.user_id"] missing_indexes: ['users.email', 'orders.user_id']
caching: caching:
hit_rate: 0% hit_rate: 0%
recommendation: "Add Redis for session data" recommendation: 'Add Redis for session data'
load_test: load_test:
max_rps: 150 max_rps: 150
breaking_point: 200 rps breaking_point: 200 rps
``` ```
</details> </details>

View File

@ -27,11 +27,11 @@ Slug rules:
```yaml ```yaml
schema: 1 schema: 1
story: "{epic}.{story}" story: '{epic}.{story}'
gate: PASS|CONCERNS|FAIL|WAIVED gate: PASS|CONCERNS|FAIL|WAIVED
status_reason: "1-2 sentence explanation of gate decision" status_reason: '1-2 sentence explanation of gate decision'
reviewer: "Quinn" reviewer: 'Quinn'
updated: "{ISO-8601 timestamp}" updated: '{ISO-8601 timestamp}'
top_issues: [] # Empty array if no issues top_issues: [] # Empty array if no issues
waiver: { active: false } # Only set active: true if WAIVED waiver: { active: false } # Only set active: true if WAIVED
``` ```
@ -40,20 +40,20 @@ waiver: { active: false } # Only set active: true if WAIVED
```yaml ```yaml
schema: 1 schema: 1
story: "1.3" story: '1.3'
gate: CONCERNS gate: CONCERNS
status_reason: "Missing rate limiting on auth endpoints poses security risk." status_reason: 'Missing rate limiting on auth endpoints poses security risk.'
reviewer: "Quinn" reviewer: 'Quinn'
updated: "2025-01-12T10:15:00Z" updated: '2025-01-12T10:15:00Z'
top_issues: top_issues:
- id: "SEC-001" - id: 'SEC-001'
severity: high # ONLY: low|medium|high severity: high # ONLY: low|medium|high
finding: "No rate limiting on login endpoint" finding: 'No rate limiting on login endpoint'
suggested_action: "Add rate limiting middleware before production" suggested_action: 'Add rate limiting middleware before production'
- id: "TEST-001" - id: 'TEST-001'
severity: medium severity: medium
finding: "No integration tests for auth flow" finding: 'No integration tests for auth flow'
suggested_action: "Add integration test coverage" suggested_action: 'Add integration test coverage'
waiver: { active: false } waiver: { active: false }
``` ```
@ -61,20 +61,20 @@ waiver: { active: false }
```yaml ```yaml
schema: 1 schema: 1
story: "1.3" story: '1.3'
gate: WAIVED gate: WAIVED
status_reason: "Known issues accepted for MVP release." status_reason: 'Known issues accepted for MVP release.'
reviewer: "Quinn" reviewer: 'Quinn'
updated: "2025-01-12T10:15:00Z" updated: '2025-01-12T10:15:00Z'
top_issues: top_issues:
- id: "PERF-001" - id: 'PERF-001'
severity: low severity: low
finding: "Dashboard loads slowly with 1000+ items" finding: 'Dashboard loads slowly with 1000+ items'
suggested_action: "Implement pagination in next sprint" suggested_action: 'Implement pagination in next sprint'
waiver: waiver:
active: true active: true
reason: "MVP release - performance optimization deferred" reason: 'MVP release - performance optimization deferred'
approved_by: "Product Owner" approved_by: 'Product Owner'
``` ```
## Gate Decision Criteria ## Gate Decision Criteria

View File

@ -6,10 +6,10 @@ Perform a comprehensive test architecture review with quality gate decision. Thi
```yaml ```yaml
required: required:
- story_id: "{epic}.{story}" # e.g., "1.3" - story_id: '{epic}.{story}' # e.g., "1.3"
- story_path: "{devStoryLocation}/{epic}.{story}.*.md" # Path from core-config.yaml - story_path: '{devStoryLocation}/{epic}.{story}.*.md' # Path from core-config.yaml
- story_title: "{title}" # If missing, derive from story file H1 - story_title: '{title}' # If missing, derive from story file H1
- story_slug: "{slug}" # If missing, derive from title (lowercase, hyphenated) - story_slug: '{slug}' # If missing, derive from title (lowercase, hyphenated)
``` ```
## Prerequisites ## Prerequisites
@ -191,19 +191,19 @@ Gate file structure:
```yaml ```yaml
schema: 1 schema: 1
story: "{epic}.{story}" story: '{epic}.{story}'
story_title: "{story title}" story_title: '{story title}'
gate: PASS|CONCERNS|FAIL|WAIVED gate: PASS|CONCERNS|FAIL|WAIVED
status_reason: "1-2 sentence explanation of gate decision" status_reason: '1-2 sentence explanation of gate decision'
reviewer: "Quinn (Test Architect)" reviewer: 'Quinn (Test Architect)'
updated: "{ISO-8601 timestamp}" updated: '{ISO-8601 timestamp}'
top_issues: [] # Empty if no issues top_issues: [] # Empty if no issues
waiver: { active: false } # Set active: true only if WAIVED waiver: { active: false } # Set active: true only if WAIVED
# Extended fields (optional but recommended): # Extended fields (optional but recommended):
quality_score: 0-100 # 100 - (20*FAILs) - (10*CONCERNS) or use technical-preferences.md weights quality_score: 0-100 # 100 - (20*FAILs) - (10*CONCERNS) or use technical-preferences.md weights
expires: "{ISO-8601 timestamp}" # Typically 2 weeks from review expires: '{ISO-8601 timestamp}' # Typically 2 weeks from review
evidence: evidence:
tests_reviewed: { count } tests_reviewed: { count }
@ -215,24 +215,24 @@ evidence:
nfr_validation: nfr_validation:
security: security:
status: PASS|CONCERNS|FAIL status: PASS|CONCERNS|FAIL
notes: "Specific findings" notes: 'Specific findings'
performance: performance:
status: PASS|CONCERNS|FAIL status: PASS|CONCERNS|FAIL
notes: "Specific findings" notes: 'Specific findings'
reliability: reliability:
status: PASS|CONCERNS|FAIL status: PASS|CONCERNS|FAIL
notes: "Specific findings" notes: 'Specific findings'
maintainability: maintainability:
status: PASS|CONCERNS|FAIL status: PASS|CONCERNS|FAIL
notes: "Specific findings" notes: 'Specific findings'
recommendations: recommendations:
immediate: # Must fix before production immediate: # Must fix before production
- action: "Add rate limiting" - action: 'Add rate limiting'
refs: ["api/auth/login.ts"] refs: ['api/auth/login.ts']
future: # Can be addressed later future: # Can be addressed later
- action: "Consider caching" - action: 'Consider caching'
refs: ["services/data.ts"] refs: ['services/data.ts']
``` ```
### Gate Decision Criteria ### Gate Decision Criteria

View File

@ -6,10 +6,10 @@ Generate a comprehensive risk assessment matrix for a story implementation using
```yaml ```yaml
required: required:
- story_id: "{epic}.{story}" # e.g., "1.3" - story_id: '{epic}.{story}' # e.g., "1.3"
- story_path: "docs/stories/{epic}.{story}.*.md" - story_path: 'docs/stories/{epic}.{story}.*.md'
- story_title: "{title}" # If missing, derive from story file H1 - story_title: '{title}' # If missing, derive from story file H1
- story_slug: "{slug}" # If missing, derive from title (lowercase, hyphenated) - story_slug: '{slug}' # If missing, derive from title (lowercase, hyphenated)
``` ```
## Purpose ## Purpose
@ -79,14 +79,14 @@ For each category, identify specific risks:
```yaml ```yaml
risk: risk:
id: "SEC-001" # Use prefixes: SEC, PERF, DATA, BUS, OPS, TECH id: 'SEC-001' # Use prefixes: SEC, PERF, DATA, BUS, OPS, TECH
category: security category: security
title: "Insufficient input validation on user forms" title: 'Insufficient input validation on user forms'
description: "Form inputs not properly sanitized could lead to XSS attacks" description: 'Form inputs not properly sanitized could lead to XSS attacks'
affected_components: affected_components:
- "UserRegistrationForm" - 'UserRegistrationForm'
- "ProfileUpdateForm" - 'ProfileUpdateForm'
detection_method: "Code review revealed missing validation" detection_method: 'Code review revealed missing validation'
``` ```
### 2. Risk Assessment ### 2. Risk Assessment
@ -133,20 +133,20 @@ For each identified risk, provide mitigation:
```yaml ```yaml
mitigation: mitigation:
risk_id: "SEC-001" risk_id: 'SEC-001'
strategy: "preventive" # preventive|detective|corrective strategy: 'preventive' # preventive|detective|corrective
actions: actions:
- "Implement input validation library (e.g., validator.js)" - 'Implement input validation library (e.g., validator.js)'
- "Add CSP headers to prevent XSS execution" - 'Add CSP headers to prevent XSS execution'
- "Sanitize all user inputs before storage" - 'Sanitize all user inputs before storage'
- "Escape all outputs in templates" - 'Escape all outputs in templates'
testing_requirements: testing_requirements:
- "Security testing with OWASP ZAP" - 'Security testing with OWASP ZAP'
- "Manual penetration testing of forms" - 'Manual penetration testing of forms'
- "Unit tests for validation functions" - 'Unit tests for validation functions'
residual_risk: "Low - Some zero-day vulnerabilities may remain" residual_risk: 'Low - Some zero-day vulnerabilities may remain'
owner: "dev" owner: 'dev'
timeline: "Before deployment" timeline: 'Before deployment'
``` ```
## Outputs ## Outputs
@ -172,12 +172,12 @@ risk_summary:
highest: highest:
id: SEC-001 id: SEC-001
score: 9 score: 9
title: "XSS on profile form" title: 'XSS on profile form'
recommendations: recommendations:
must_fix: must_fix:
- "Add input sanitization & CSP" - 'Add input sanitization & CSP'
monitor: monitor:
- "Add security alerts for auth endpoints" - 'Add security alerts for auth endpoints'
``` ```
### Output 2: Markdown Report ### Output 2: Markdown Report

View File

@ -6,10 +6,10 @@ Create comprehensive test scenarios with appropriate test level recommendations
```yaml ```yaml
required: required:
- story_id: "{epic}.{story}" # e.g., "1.3" - story_id: '{epic}.{story}' # e.g., "1.3"
- story_path: "{devStoryLocation}/{epic}.{story}.*.md" # Path from core-config.yaml - story_path: '{devStoryLocation}/{epic}.{story}.*.md' # Path from core-config.yaml
- story_title: "{title}" # If missing, derive from story file H1 - story_title: '{title}' # If missing, derive from story file H1
- story_slug: "{slug}" # If missing, derive from title (lowercase, hyphenated) - story_slug: '{slug}' # If missing, derive from title (lowercase, hyphenated)
``` ```
## Purpose ## Purpose
@ -62,13 +62,13 @@ For each identified test need, create:
```yaml ```yaml
test_scenario: test_scenario:
id: "{epic}.{story}-{LEVEL}-{SEQ}" id: '{epic}.{story}-{LEVEL}-{SEQ}'
requirement: "AC reference" requirement: 'AC reference'
priority: P0|P1|P2|P3 priority: P0|P1|P2|P3
level: unit|integration|e2e level: unit|integration|e2e
description: "What is being tested" description: 'What is being tested'
justification: "Why this level was chosen" justification: 'Why this level was chosen'
mitigates_risks: ["RISK-001"] # If risk profile exists mitigates_risks: ['RISK-001'] # If risk profile exists
``` ```
### 5. Validate Coverage ### 5. Validate Coverage

View File

@ -31,21 +31,21 @@ Identify all testable requirements from:
For each requirement, document which tests validate it. Use Given-When-Then to describe what the test validates (not how it's written): For each requirement, document which tests validate it. Use Given-When-Then to describe what the test validates (not how it's written):
```yaml ```yaml
requirement: "AC1: User can login with valid credentials" requirement: 'AC1: User can login with valid credentials'
test_mappings: test_mappings:
- test_file: "auth/login.test.ts" - test_file: 'auth/login.test.ts'
test_case: "should successfully login with valid email and password" test_case: 'should successfully login with valid email and password'
# Given-When-Then describes WHAT the test validates, not HOW it's coded # Given-When-Then describes WHAT the test validates, not HOW it's coded
given: "A registered user with valid credentials" given: 'A registered user with valid credentials'
when: "They submit the login form" when: 'They submit the login form'
then: "They are redirected to dashboard and session is created" then: 'They are redirected to dashboard and session is created'
coverage: full coverage: full
- test_file: "e2e/auth-flow.test.ts" - test_file: 'e2e/auth-flow.test.ts'
test_case: "complete login flow" test_case: 'complete login flow'
given: "User on login page" given: 'User on login page'
when: "Entering valid credentials and submitting" when: 'Entering valid credentials and submitting'
then: "Dashboard loads with user data" then: 'Dashboard loads with user data'
coverage: integration coverage: integration
``` ```
@ -67,19 +67,19 @@ Document any gaps found:
```yaml ```yaml
coverage_gaps: coverage_gaps:
- requirement: "AC3: Password reset email sent within 60 seconds" - requirement: 'AC3: Password reset email sent within 60 seconds'
gap: "No test for email delivery timing" gap: 'No test for email delivery timing'
severity: medium severity: medium
suggested_test: suggested_test:
type: integration type: integration
description: "Test email service SLA compliance" description: 'Test email service SLA compliance'
- requirement: "AC5: Support 1000 concurrent users" - requirement: 'AC5: Support 1000 concurrent users'
gap: "No load testing implemented" gap: 'No load testing implemented'
severity: high severity: high
suggested_test: suggested_test:
type: performance type: performance
description: "Load test with 1000 concurrent connections" description: 'Load test with 1000 concurrent connections'
``` ```
## Outputs ## Outputs
@ -95,11 +95,11 @@ trace:
full: Y full: Y
partial: Z partial: Z
none: W none: W
planning_ref: "docs/qa/assessments/{epic}.{story}-test-design-{YYYYMMDD}.md" planning_ref: 'docs/qa/assessments/{epic}.{story}-test-design-{YYYYMMDD}.md'
uncovered: uncovered:
- ac: "AC3" - ac: 'AC3'
reason: "No test found for password reset timing" reason: 'No test found for password reset timing'
notes: "See docs/qa/assessments/{epic}.{story}-trace-{YYYYMMDD}.md" notes: 'See docs/qa/assessments/{epic}.{story}-trace-{YYYYMMDD}.md'
``` ```
### Output 2: Traceability Report ### Output 2: Traceability Report

View File

@ -141,7 +141,14 @@ sections:
title: Feature Comparison Matrix title: Feature Comparison Matrix
instruction: Create a detailed comparison table of key features across competitors instruction: Create a detailed comparison table of key features across competitors
type: table type: table
columns: ["Feature Category", "{{your_company}}", "{{competitor_1}}", "{{competitor_2}}", "{{competitor_3}}"] columns:
[
"Feature Category",
"{{your_company}}",
"{{competitor_1}}",
"{{competitor_2}}",
"{{competitor_3}}",
]
rows: rows:
- category: "Core Functionality" - category: "Core Functionality"
items: items:
@ -153,7 +160,13 @@ sections:
- ["Onboarding Time", "{{time}}", "{{time}}", "{{time}}", "{{time}}"] - ["Onboarding Time", "{{time}}", "{{time}}", "{{time}}", "{{time}}"]
- category: "Integration & Ecosystem" - category: "Integration & Ecosystem"
items: items:
- ["API Availability", "{{availability}}", "{{availability}}", "{{availability}}", "{{availability}}"] - [
"API Availability",
"{{availability}}",
"{{availability}}",
"{{availability}}",
"{{availability}}",
]
- ["Third-party Integrations", "{{number}}", "{{number}}", "{{number}}", "{{number}}"] - ["Third-party Integrations", "{{number}}", "{{number}}", "{{number}}", "{{number}}"]
- category: "Pricing & Plans" - category: "Pricing & Plans"
items: items:

View File

@ -75,12 +75,24 @@ sections:
rows: rows:
- ["Framework", "{{framework}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Framework", "{{framework}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["UI Library", "{{ui_library}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["UI Library", "{{ui_library}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["State Management", "{{state_management}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - [
"State Management",
"{{state_management}}",
"{{version}}",
"{{purpose}}",
"{{why_chosen}}",
]
- ["Routing", "{{routing_library}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Routing", "{{routing_library}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Build Tool", "{{build_tool}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Build Tool", "{{build_tool}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Styling", "{{styling_solution}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Styling", "{{styling_solution}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Testing", "{{test_framework}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Testing", "{{test_framework}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Component Library", "{{component_lib}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - [
"Component Library",
"{{component_lib}}",
"{{version}}",
"{{purpose}}",
"{{why_chosen}}",
]
- ["Form Handling", "{{form_library}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Form Handling", "{{form_library}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Animation", "{{animation_lib}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Animation", "{{animation_lib}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Dev Tools", "{{dev_tools}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Dev Tools", "{{dev_tools}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]

View File

@ -156,11 +156,29 @@ sections:
columns: [Category, Technology, Version, Purpose, Rationale] columns: [Category, Technology, Version, Purpose, Rationale]
rows: rows:
- ["Frontend Language", "{{fe_language}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Frontend Language", "{{fe_language}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Frontend Framework", "{{fe_framework}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - [
- ["UI Component Library", "{{ui_library}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] "Frontend Framework",
"{{fe_framework}}",
"{{version}}",
"{{purpose}}",
"{{why_chosen}}",
]
- [
"UI Component Library",
"{{ui_library}}",
"{{version}}",
"{{purpose}}",
"{{why_chosen}}",
]
- ["State Management", "{{state_mgmt}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["State Management", "{{state_mgmt}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Backend Language", "{{be_language}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Backend Language", "{{be_language}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Backend Framework", "{{be_framework}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - [
"Backend Framework",
"{{be_framework}}",
"{{version}}",
"{{purpose}}",
"{{why_chosen}}",
]
- ["API Style", "{{api_style}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["API Style", "{{api_style}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Database", "{{database}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Database", "{{database}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
- ["Cache", "{{cache}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"] - ["Cache", "{{cache}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]

View File

@ -14,7 +14,7 @@ template:
output: output:
format: markdown format: markdown
filename: default-path/to/{{filename}}.md filename: default-path/to/{{filename}}.md
title: "{{variable}} Document Title" title: '{{variable}} Document Title'
workflow: workflow:
mode: interactive mode: interactive
@ -108,8 +108,8 @@ sections:
Use `{{variable_name}}` in titles, templates, and content: Use `{{variable_name}}` in titles, templates, and content:
```yaml ```yaml
title: "Epic {{epic_number}} {{epic_title}}" title: 'Epic {{epic_number}} {{epic_title}}'
template: "As a {{user_type}}, I want {{action}}, so that {{benefit}}." template: 'As a {{user_type}}, I want {{action}}, so that {{benefit}}.'
``` ```
### Conditional Sections ### Conditional Sections
@ -212,7 +212,7 @@ choices:
- id: criteria - id: criteria
title: Acceptance Criteria title: Acceptance Criteria
type: numbered-list type: numbered-list
item_template: "{{criterion_number}}: {{criteria}}" item_template: '{{criterion_number}}: {{criteria}}'
repeatable: true repeatable: true
``` ```
@ -220,7 +220,7 @@ choices:
````yaml ````yaml
examples: examples:
- "FR6: The system must authenticate users within 2 seconds" - 'FR6: The system must authenticate users within 2 seconds'
- | - |
```mermaid ```mermaid
sequenceDiagram sequenceDiagram

View File

@ -2328,7 +2328,7 @@ You are the "Vibe CEO" - thinking like a CEO with unlimited resources and a sing
- **Claude Code**: `/agent-name` (e.g., `/bmad-master`) - **Claude Code**: `/agent-name` (e.g., `/bmad-master`)
- **Cursor**: `@agent-name` (e.g., `@bmad-master`) - **Cursor**: `@agent-name` (e.g., `@bmad-master`)
- **Windsurf**: `@agent-name` (e.g., `@bmad-master`) - **Windsurf**: `/agent-name` (e.g., `/bmad-master`)
- **Trae**: `@agent-name` (e.g., `@bmad-master`) - **Trae**: `@agent-name` (e.g., `@bmad-master`)
- **Roo Code**: Select mode from mode selector (e.g., `bmad-master`) - **Roo Code**: Select mode from mode selector (e.g., `bmad-master`)
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector.

View File

@ -8015,7 +8015,7 @@ You are the "Vibe CEO" - thinking like a CEO with unlimited resources and a sing
- **Claude Code**: `/agent-name` (e.g., `/bmad-master`) - **Claude Code**: `/agent-name` (e.g., `/bmad-master`)
- **Cursor**: `@agent-name` (e.g., `@bmad-master`) - **Cursor**: `@agent-name` (e.g., `@bmad-master`)
- **Windsurf**: `@agent-name` (e.g., `@bmad-master`) - **Windsurf**: `/agent-name` (e.g., `/bmad-master`)
- **Trae**: `@agent-name` (e.g., `@bmad-master`) - **Trae**: `@agent-name` (e.g., `@bmad-master`)
- **Roo Code**: Select mode from mode selector (e.g., `bmad-master`) - **Roo Code**: Select mode from mode selector (e.g., `bmad-master`)
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector.

View File

@ -775,7 +775,7 @@ You are the "Vibe CEO" - thinking like a CEO with unlimited resources and a sing
- **Claude Code**: `/agent-name` (e.g., `/bmad-master`) - **Claude Code**: `/agent-name` (e.g., `/bmad-master`)
- **Cursor**: `@agent-name` (e.g., `@bmad-master`) - **Cursor**: `@agent-name` (e.g., `@bmad-master`)
- **Windsurf**: `@agent-name` (e.g., `@bmad-master`) - **Windsurf**: `/agent-name` (e.g., `/bmad-master`)
- **Trae**: `@agent-name` (e.g., `@bmad-master`) - **Trae**: `@agent-name` (e.g., `@bmad-master`)
- **Roo Code**: Select mode from mode selector (e.g., `bmad-master`) - **Roo Code**: Select mode from mode selector (e.g., `bmad-master`)
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector.

View File

@ -3698,7 +3698,7 @@ Use the `shard-doc` task or `@kayvan/markdown-tree-parser` tool for automatic ga
- **Claude Code**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect` - **Claude Code**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect`
- **Cursor**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Cursor**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect`
- **Windsurf**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Windsurf**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect`
- **Trae**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Trae**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect`
- **Roo Code**: Select mode from mode selector with bmad2du prefix - **Roo Code**: Select mode from mode selector with bmad2du prefix
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select the appropriate game agent. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select the appropriate game agent.

View File

@ -3384,7 +3384,7 @@ Use the `shard-doc` task or `@kayvan/markdown-tree-parser` tool for automatic ga
- **Claude Code**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect` - **Claude Code**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect`
- **Cursor**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Cursor**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect`
- **Windsurf**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Windsurf**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect`
- **Trae**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Trae**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect`
- **Roo Code**: Select mode from mode selector with bmad2du prefix - **Roo Code**: Select mode from mode selector with bmad2du prefix
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select the appropriate game agent. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select the appropriate game agent.

View File

@ -2857,7 +2857,7 @@ Use the `shard-doc` task or `@kayvan/markdown-tree-parser` tool for automatic ga
- **Claude Code**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect` - **Claude Code**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect`
- **Cursor**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Cursor**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect`
- **Windsurf**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Windsurf**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect`
- **Trae**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Trae**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect`
- **Roo Code**: Select mode from mode selector with bmad2du prefix - **Roo Code**: Select mode from mode selector with bmad2du prefix
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select the appropriate game agent. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select the appropriate game agent.
@ -14460,7 +14460,7 @@ Use the `shard-doc` task or `@kayvan/markdown-tree-parser` tool for automatic ga
- **Claude Code**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect` - **Claude Code**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect`
- **Cursor**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Cursor**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect`
- **Windsurf**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Windsurf**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect`
- **Trae**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Trae**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect`
- **Roo Code**: Select mode from mode selector with bmad2du prefix - **Roo Code**: Select mode from mode selector with bmad2du prefix
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select the appropriate game agent. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select the appropriate game agent.

View File

@ -1261,7 +1261,7 @@ You are the "Vibe CEO" - thinking like a CEO with unlimited resources and a sing
- **Claude Code**: `/agent-name` (e.g., `/bmad-master`) - **Claude Code**: `/agent-name` (e.g., `/bmad-master`)
- **Cursor**: `@agent-name` (e.g., `@bmad-master`) - **Cursor**: `@agent-name` (e.g., `@bmad-master`)
- **Windsurf**: `@agent-name` (e.g., `@bmad-master`) - **Windsurf**: `/agent-name` (e.g., `/bmad-master`)
- **Trae**: `@agent-name` (e.g., `@bmad-master`) - **Trae**: `@agent-name` (e.g., `@bmad-master`)
- **Roo Code**: Select mode from mode selector (e.g., `bmad-master`) - **Roo Code**: Select mode from mode selector (e.g., `bmad-master`)
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector.

View File

@ -1098,7 +1098,7 @@ You are the "Vibe CEO" - thinking like a CEO with unlimited resources and a sing
- **Claude Code**: `/agent-name` (e.g., `/bmad-master`) - **Claude Code**: `/agent-name` (e.g., `/bmad-master`)
- **Cursor**: `@agent-name` (e.g., `@bmad-master`) - **Cursor**: `@agent-name` (e.g., `@bmad-master`)
- **Windsurf**: `@agent-name` (e.g., `@bmad-master`) - **Windsurf**: `/agent-name` (e.g., `/bmad-master`)
- **Trae**: `@agent-name` (e.g., `@bmad-master`) - **Trae**: `@agent-name` (e.g., `@bmad-master`)
- **Roo Code**: Select mode from mode selector (e.g., `bmad-master`) - **Roo Code**: Select mode from mode selector (e.g., `bmad-master`)
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector.

View File

@ -1014,7 +1014,7 @@ You are the "Vibe CEO" - thinking like a CEO with unlimited resources and a sing
- **Claude Code**: `/agent-name` (e.g., `/bmad-master`) - **Claude Code**: `/agent-name` (e.g., `/bmad-master`)
- **Cursor**: `@agent-name` (e.g., `@bmad-master`) - **Cursor**: `@agent-name` (e.g., `@bmad-master`)
- **Windsurf**: `@agent-name` (e.g., `@bmad-master`) - **Windsurf**: `/agent-name` (e.g., `/bmad-master`)
- **Trae**: `@agent-name` (e.g., `@bmad-master`) - **Trae**: `@agent-name` (e.g., `@bmad-master`)
- **Roo Code**: Select mode from mode selector (e.g., `bmad-master`) - **Roo Code**: Select mode from mode selector (e.g., `bmad-master`)
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector.

View File

@ -1044,7 +1044,7 @@ You are the "Vibe CEO" - thinking like a CEO with unlimited resources and a sing
- **Claude Code**: `/agent-name` (e.g., `/bmad-master`) - **Claude Code**: `/agent-name` (e.g., `/bmad-master`)
- **Cursor**: `@agent-name` (e.g., `@bmad-master`) - **Cursor**: `@agent-name` (e.g., `@bmad-master`)
- **Windsurf**: `@agent-name` (e.g., `@bmad-master`) - **Windsurf**: `/agent-name` (e.g., `/bmad-master`)
- **Trae**: `@agent-name` (e.g., `@bmad-master`) - **Trae**: `@agent-name` (e.g., `@bmad-master`)
- **Roo Code**: Select mode from mode selector (e.g., `bmad-master`) - **Roo Code**: Select mode from mode selector (e.g., `bmad-master`)
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select **Agent** from the chat mode selector.

View File

@ -30,7 +30,7 @@ The Test Architect (Quinn) provides comprehensive quality assurance throughout t
### Quick Command Reference ### Quick Command Reference
| **Stage** | **Command** | **Purpose** | **Output** | **Priority** | | **Stage** | **Command** | **Purpose** | **Output** | **Priority** |
|-----------|------------|-------------|------------|--------------| | ------------------------ | ----------- | --------------------------------------- | --------------------------------------------------------------- | --------------------------- |
| **After Story Approval** | `*risk` | Identify integration & regression risks | `docs/qa/assessments/{epic}.{story}-risk-{YYYYMMDD}.md` | High for complex/brownfield | | **After Story Approval** | `*risk` | Identify integration & regression risks | `docs/qa/assessments/{epic}.{story}-risk-{YYYYMMDD}.md` | High for complex/brownfield |
| | `*design` | Create test strategy for dev | `docs/qa/assessments/{epic}.{story}-test-design-{YYYYMMDD}.md` | High for new features | | | `*design` | Create test strategy for dev | `docs/qa/assessments/{epic}.{story}-test-design-{YYYYMMDD}.md` | High for new features |
| **During Development** | `*trace` | Verify test coverage | `docs/qa/assessments/{epic}.{story}-trace-{YYYYMMDD}.md` | Medium | | **During Development** | `*trace` | Verify test coverage | `docs/qa/assessments/{epic}.{story}-trace-{YYYYMMDD}.md` | Medium |
@ -135,7 +135,7 @@ The Test Architect (Quinn) provides comprehensive quality assurance throughout t
### Understanding Gate Decisions ### Understanding Gate Decisions
| **Status** | **Meaning** | **Action Required** | **Can Proceed?** | | **Status** | **Meaning** | **Action Required** | **Can Proceed?** |
|------------|-------------|-------------------|------------------| | ------------ | -------------------------------------------- | ----------------------- | ---------------- |
| **PASS** | All critical requirements met | None | ✅ Yes | | **PASS** | All critical requirements met | None | ✅ Yes |
| **CONCERNS** | Non-critical issues found | Team review recommended | ⚠️ With caution | | **CONCERNS** | Non-critical issues found | Team review recommended | ⚠️ With caution |
| **FAIL** | Critical issues (security, missing P0 tests) | Must fix | ❌ No | | **FAIL** | Critical issues (security, missing P0 tests) | Must fix | ❌ No |
@ -146,7 +146,7 @@ The Test Architect (Quinn) provides comprehensive quality assurance throughout t
The Test Architect uses risk scoring to prioritize testing: The Test Architect uses risk scoring to prioritize testing:
| **Risk Score** | **Calculation** | **Testing Priority** | **Gate Impact** | | **Risk Score** | **Calculation** | **Testing Priority** | **Gate Impact** |
|---------------|----------------|-------------------|----------------| | -------------- | ------------------------------ | ------------------------- | ------------------------ |
| **9** | High probability × High impact | P0 - Must test thoroughly | FAIL if untested | | **9** | High probability × High impact | P0 - Must test thoroughly | FAIL if untested |
| **6** | Medium-high combinations | P1 - Should test well | CONCERNS if gaps | | **6** | Medium-high combinations | P1 - Should test well | CONCERNS if gaps |
| **4** | Medium combinations | P1 - Should test | CONCERNS if notable gaps | | **4** | Medium combinations | P1 - Should test | CONCERNS if notable gaps |
@ -228,7 +228,7 @@ All Test Architect activities create permanent records:
**Should I run Test Architect commands?** **Should I run Test Architect commands?**
| **Scenario** | **Before Dev** | **During Dev** | **After Dev** | | **Scenario** | **Before Dev** | **During Dev** | **After Dev** |
|-------------|---------------|----------------|---------------| | ------------------------ | ------------------------------- | ---------------------------- | ---------------------------- |
| **Simple bug fix** | Optional | Optional | Required `*review` | | **Simple bug fix** | Optional | Optional | Required `*review` |
| **New feature** | Recommended `*risk`, `*design` | Optional `*trace` | Required `*review` | | **New feature** | Recommended `*risk`, `*design` | Optional `*trace` | Required `*review` |
| **Brownfield change** | **Required** `*risk`, `*design` | Recommended `*trace`, `*nfr` | Required `*review` | | **Brownfield change** | **Required** `*risk`, `*design` | Recommended `*trace`, `*nfr` | Required `*review` |

View File

@ -377,7 +377,7 @@ Manages quality gate decisions:
The Test Architect provides value throughout the entire development lifecycle. Here's when and how to leverage each capability: The Test Architect provides value throughout the entire development lifecycle. Here's when and how to leverage each capability:
| **Stage** | **Command** | **When to Use** | **Value** | **Output** | | **Stage** | **Command** | **When to Use** | **Value** | **Output** |
|-----------|------------|-----------------|-----------|------------| | ------------------ | ----------- | ----------------------- | -------------------------- | -------------------------------------------------------------- |
| **Story Drafting** | `*risk` | After SM drafts story | Identify pitfalls early | `docs/qa/assessments/{epic}.{story}-risk-{YYYYMMDD}.md` | | **Story Drafting** | `*risk` | After SM drafts story | Identify pitfalls early | `docs/qa/assessments/{epic}.{story}-risk-{YYYYMMDD}.md` |
| | `*design` | After risk assessment | Guide dev on test strategy | `docs/qa/assessments/{epic}.{story}-test-design-{YYYYMMDD}.md` | | | `*design` | After risk assessment | Guide dev on test strategy | `docs/qa/assessments/{epic}.{story}-test-design-{YYYYMMDD}.md` |
| **Development** | `*trace` | Mid-implementation | Verify test coverage | `docs/qa/assessments/{epic}.{story}-trace-{YYYYMMDD}.md` | | **Development** | `*trace` | Mid-implementation | Verify test coverage | `docs/qa/assessments/{epic}.{story}-trace-{YYYYMMDD}.md` |

105
eslint.config.mjs Normal file
View File

@ -0,0 +1,105 @@
import js from '@eslint/js';
import nodePlugin from 'eslint-plugin-n';
import yml from 'eslint-plugin-yml';
import unicorn from 'eslint-plugin-unicorn';
import eslintConfigPrettier from 'eslint-config-prettier/flat';
export default [
// Global ignores for files/folders that should not be linted
{
ignores: ['dist/**', 'coverage/**', '**/*.min.js'],
},
// Base JavaScript recommended rules
js.configs.recommended,
// Node.js rules
...nodePlugin.configs['flat/mixed-esm-and-cjs'],
// Unicorn rules (modern best practices)
unicorn.configs.recommended,
// YAML linting
...yml.configs['flat/recommended'],
// Place Prettier last to disable conflicting stylistic rules
eslintConfigPrettier,
// Project-specific tweaks
{
rules: {
// Allow console for CLI tools in this repo
'no-console': 'off',
// Do not enforce a specific YAML file extension (.yml vs .yaml)
'yml/file-extension': 'off',
// Relax some Unicorn rules that are too opinionated for this codebase
'unicorn/prevent-abbreviations': 'off',
'unicorn/no-null': 'off',
},
},
// CLI/CommonJS scripts under tools/**
{
files: ['tools/**/*.js'],
rules: {
// Allow CommonJS patterns for Node CLI scripts
'unicorn/prefer-module': 'off',
'unicorn/import-style': 'off',
'unicorn/no-process-exit': 'off',
'n/no-process-exit': 'off',
'unicorn/no-await-expression-member': 'off',
'unicorn/prefer-top-level-await': 'off',
// Avoid failing CI on incidental unused vars in internal scripts
'no-unused-vars': 'off',
// Reduce style-only churn in internal tools
'unicorn/prefer-ternary': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-array-reduce': 'off',
'unicorn/no-array-callback-reference': 'off',
'unicorn/consistent-function-scoping': 'off',
'n/no-extraneous-require': 'off',
'n/no-extraneous-import': 'off',
'n/no-unpublished-require': 'off',
'n/no-unpublished-import': 'off',
// Some scripts intentionally use globals provided at runtime
'no-undef': 'off',
// Additional relaxed rules for legacy/internal scripts
'no-useless-catch': 'off',
'unicorn/prefer-number-properties': 'off',
'no-unreachable': 'off',
},
},
// ESLint config file should not be checked for publish-related Node rules
{
files: ['eslint.config.mjs'],
rules: {
'n/no-unpublished-import': 'off',
},
},
// YAML workflow templates allow empty mapping values intentionally
{
files: ['bmad-core/workflows/**/*.{yml,yaml}'],
rules: {
'yml/no-empty-mapping-value': 'off',
},
},
// GitHub workflow files in this repo may use empty mapping values
{
files: ['.github/workflows/**/*.{yml,yaml}'],
rules: {
'yml/no-empty-mapping-value': 'off',
},
},
// Other GitHub YAML files may intentionally use empty values and reserved filenames
{
files: ['.github/**/*.{yml,yaml}'],
rules: {
'yml/no-empty-mapping-value': 'off',
'unicorn/filename-case': 'off',
},
},
];

View File

@ -1,26 +1,26 @@
steps: steps:
# Build the container image # Build the container image
- name: 'gcr.io/cloud-builders/docker' - name: "gcr.io/cloud-builders/docker"
args: ['build', '-t', 'gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA', '.'] args: ["build", "-t", "gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA", "."]
# Push the container image to Container Registry # Push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker' - name: "gcr.io/cloud-builders/docker"
args: ['push', 'gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA'] args: ["push", "gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA"]
# Deploy container image to Cloud Run # Deploy container image to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' - name: "gcr.io/google.com/cloudsdktool/cloud-sdk"
entrypoint: gcloud entrypoint: gcloud
args: args:
- 'run' - "run"
- 'deploy' - "deploy"
- '{{COMPANY_NAME}}-ai-agents' - "{{COMPANY_NAME}}-ai-agents"
- '--image' - "--image"
- 'gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA' - "gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA"
- '--region' - "--region"
- '{{LOCATION}}' - "{{LOCATION}}"
- '--platform' - "--platform"
- 'managed' - "managed"
- '--allow-unauthenticated' - "--allow-unauthenticated"
images: images:
- 'gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA' - "gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA"

View File

@ -60,10 +60,10 @@ commands:
task-execution: task-execution:
flow: Read story → Implement game feature → Write tests → Pass tests → Update [x] → Next task flow: Read story → Implement game feature → Write tests → Pass tests → Update [x] → Next task
updates-ONLY: updates-ONLY:
- "Checkboxes: [ ] not started | [-] in progress | [x] complete" - 'Checkboxes: [ ] not started | [-] in progress | [x] complete'
- "Debug Log: | Task | File | Change | Reverted? |" - 'Debug Log: | Task | File | Change | Reverted? |'
- "Completion Notes: Deviations only, <50 words" - 'Completion Notes: Deviations only, <50 words'
- "Change Log: Requirement changes only" - 'Change Log: Requirement changes only'
blocking: Unapproved deps | Ambiguous after story check | 3 failures | Missing game config blocking: Unapproved deps | Ambiguous after story check | 3 failures | Missing game config
done: Game feature works + Tests pass + 60 FPS + No lint errors + Follows Phaser 3 best practices done: Game feature works + Tests pass + 60 FPS + No lint errors + Follows Phaser 3 best practices
dependencies: dependencies:

View File

@ -27,7 +27,7 @@ activation-instructions:
- When listing tasks/templates or presenting options during conversations, always show as numbered options list, allowing the user to type a number to select or execute - When listing tasks/templates or presenting options during conversations, always show as numbered options list, allowing the user to type a number to select or execute
- STAY IN CHARACTER! - STAY IN CHARACTER!
- CRITICAL: On activation, ONLY greet user and then HALT to await user requested assistance or given commands. ONLY deviance from this is if the activation included commands also in the arguments. - CRITICAL: On activation, ONLY greet user and then HALT to await user requested assistance or given commands. ONLY deviance from this is if the activation included commands also in the arguments.
- "CRITICAL RULE: You are ONLY allowed to create/modify story files - NEVER implement! If asked to implement, tell user they MUST switch to Game Developer Agent" - 'CRITICAL RULE: You are ONLY allowed to create/modify story files - NEVER implement! If asked to implement, tell user they MUST switch to Game Developer Agent'
agent: agent:
name: Jordan name: Jordan
id: game-sm id: game-sm

View File

@ -73,7 +73,7 @@ interface GameState {
interface GameSettings { interface GameSettings {
musicVolume: number; musicVolume: number;
sfxVolume: number; sfxVolume: number;
difficulty: "easy" | "normal" | "hard"; difficulty: 'easy' | 'normal' | 'hard';
controls: ControlScheme; controls: ControlScheme;
} }
``` ```
@ -114,12 +114,12 @@ class GameScene extends Phaser.Scene {
private inputManager!: InputManager; private inputManager!: InputManager;
constructor() { constructor() {
super({ key: "GameScene" }); super({ key: 'GameScene' });
} }
preload(): void { preload(): void {
// Load only scene-specific assets // Load only scene-specific assets
this.load.image("player", "assets/player.png"); this.load.image('player', 'assets/player.png');
} }
create(data: SceneData): void { create(data: SceneData): void {
@ -144,7 +144,7 @@ class GameScene extends Phaser.Scene {
this.inputManager.destroy(); this.inputManager.destroy();
// Remove event listeners // Remove event listeners
this.events.off("*"); this.events.off('*');
} }
} }
``` ```
@ -153,13 +153,13 @@ class GameScene extends Phaser.Scene {
```typescript ```typescript
// Proper scene transitions with data // Proper scene transitions with data
this.scene.start("NextScene", { this.scene.start('NextScene', {
playerScore: this.playerScore, playerScore: this.playerScore,
currentLevel: this.currentLevel + 1, currentLevel: this.currentLevel + 1,
}); });
// Scene overlays for UI // Scene overlays for UI
this.scene.launch("PauseMenuScene"); this.scene.launch('PauseMenuScene');
this.scene.pause(); this.scene.pause();
``` ```
@ -203,7 +203,7 @@ class Player extends GameEntity {
private health!: HealthComponent; private health!: HealthComponent;
constructor(scene: Phaser.Scene, x: number, y: number) { constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, "player"); super(scene, x, y, 'player');
this.movement = this.addComponent(new MovementComponent(this)); this.movement = this.addComponent(new MovementComponent(this));
this.health = this.addComponent(new HealthComponent(this, 100)); this.health = this.addComponent(new HealthComponent(this, 100));
@ -223,7 +223,7 @@ class GameManager {
constructor(scene: Phaser.Scene) { constructor(scene: Phaser.Scene) {
if (GameManager.instance) { if (GameManager.instance) {
throw new Error("GameManager already exists!"); throw new Error('GameManager already exists!');
} }
this.scene = scene; this.scene = scene;
@ -233,7 +233,7 @@ class GameManager {
static getInstance(): GameManager { static getInstance(): GameManager {
if (!GameManager.instance) { if (!GameManager.instance) {
throw new Error("GameManager not initialized!"); throw new Error('GameManager not initialized!');
} }
return GameManager.instance; return GameManager.instance;
} }
@ -280,7 +280,7 @@ class BulletPool {
} }
// Pool exhausted - create new bullet // Pool exhausted - create new bullet
console.warn("Bullet pool exhausted, creating new bullet"); console.warn('Bullet pool exhausted, creating new bullet');
return new Bullet(this.scene, 0, 0); return new Bullet(this.scene, 0, 0);
} }
@ -380,14 +380,12 @@ class InputManager {
} }
private setupKeyboard(): void { private setupKeyboard(): void {
this.keys = this.scene.input.keyboard.addKeys( this.keys = this.scene.input.keyboard.addKeys('W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT');
"W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT",
);
} }
private setupTouch(): void { private setupTouch(): void {
this.scene.input.on("pointerdown", this.handlePointerDown, this); this.scene.input.on('pointerdown', this.handlePointerDown, this);
this.scene.input.on("pointerup", this.handlePointerUp, this); this.scene.input.on('pointerup', this.handlePointerUp, this);
} }
update(): void { update(): void {
@ -414,9 +412,9 @@ class InputManager {
class AssetManager { class AssetManager {
loadAssets(): Promise<void> { loadAssets(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.scene.load.on("filecomplete", this.handleFileComplete, this); this.scene.load.on('filecomplete', this.handleFileComplete, this);
this.scene.load.on("loaderror", this.handleLoadError, this); this.scene.load.on('loaderror', this.handleLoadError, this);
this.scene.load.on("complete", () => resolve()); this.scene.load.on('complete', () => resolve());
this.scene.load.start(); this.scene.load.start();
}); });
@ -432,8 +430,8 @@ class AssetManager {
private loadFallbackAsset(key: string): void { private loadFallbackAsset(key: string): void {
// Load placeholder or default assets // Load placeholder or default assets
switch (key) { switch (key) {
case "player": case 'player':
this.scene.load.image("player", "assets/defaults/default-player.png"); this.scene.load.image('player', 'assets/defaults/default-player.png');
break; break;
default: default:
console.warn(`No fallback for asset: ${key}`); console.warn(`No fallback for asset: ${key}`);
@ -460,11 +458,11 @@ class GameSystem {
private attemptRecovery(context: string): void { private attemptRecovery(context: string): void {
switch (context) { switch (context) {
case "update": case 'update':
// Reset system state // Reset system state
this.reset(); this.reset();
break; break;
case "render": case 'render':
// Disable visual effects // Disable visual effects
this.disableEffects(); this.disableEffects();
break; break;
@ -484,7 +482,7 @@ class GameSystem {
```typescript ```typescript
// Example test for game mechanics // Example test for game mechanics
describe("HealthComponent", () => { describe('HealthComponent', () => {
let healthComponent: HealthComponent; let healthComponent: HealthComponent;
beforeEach(() => { beforeEach(() => {
@ -492,18 +490,18 @@ describe("HealthComponent", () => {
healthComponent = new HealthComponent(mockEntity, 100); healthComponent = new HealthComponent(mockEntity, 100);
}); });
test("should initialize with correct health", () => { test('should initialize with correct health', () => {
expect(healthComponent.currentHealth).toBe(100); expect(healthComponent.currentHealth).toBe(100);
expect(healthComponent.maxHealth).toBe(100); expect(healthComponent.maxHealth).toBe(100);
}); });
test("should handle damage correctly", () => { test('should handle damage correctly', () => {
healthComponent.takeDamage(25); healthComponent.takeDamage(25);
expect(healthComponent.currentHealth).toBe(75); expect(healthComponent.currentHealth).toBe(75);
expect(healthComponent.isAlive()).toBe(true); expect(healthComponent.isAlive()).toBe(true);
}); });
test("should handle death correctly", () => { test('should handle death correctly', () => {
healthComponent.takeDamage(150); healthComponent.takeDamage(150);
expect(healthComponent.currentHealth).toBe(0); expect(healthComponent.currentHealth).toBe(0);
expect(healthComponent.isAlive()).toBe(false); expect(healthComponent.isAlive()).toBe(false);
@ -516,7 +514,7 @@ describe("HealthComponent", () => {
**Scene Testing:** **Scene Testing:**
```typescript ```typescript
describe("GameScene Integration", () => { describe('GameScene Integration', () => {
let scene: GameScene; let scene: GameScene;
let mockGame: Phaser.Game; let mockGame: Phaser.Game;
@ -526,7 +524,7 @@ describe("GameScene Integration", () => {
scene = new GameScene(); scene = new GameScene();
}); });
test("should initialize all systems", () => { test('should initialize all systems', () => {
scene.create({}); scene.create({});
expect(scene.gameManager).toBeDefined(); expect(scene.gameManager).toBeDefined();

View File

@ -17,21 +17,21 @@ workflow:
- brainstorming_session - brainstorming_session
- game_research_prompt - game_research_prompt
- player_research - player_research
notes: 'Start with brainstorming game concepts, then create comprehensive game brief. SAVE OUTPUT: Copy final game-brief.md to your project''s docs/design/ folder.' notes: "Start with brainstorming game concepts, then create comprehensive game brief. SAVE OUTPUT: Copy final game-brief.md to your project's docs/design/ folder."
- agent: game-designer - agent: game-designer
creates: game-design-doc.md creates: game-design-doc.md
requires: game-brief.md requires: game-brief.md
optional_steps: optional_steps:
- competitive_analysis - competitive_analysis
- technical_research - technical_research
notes: 'Create detailed Game Design Document using game-design-doc-tmpl. Defines all gameplay mechanics, progression, and technical requirements. SAVE OUTPUT: Copy final game-design-doc.md to your project''s docs/design/ folder.' notes: "Create detailed Game Design Document using game-design-doc-tmpl. Defines all gameplay mechanics, progression, and technical requirements. SAVE OUTPUT: Copy final game-design-doc.md to your project's docs/design/ folder."
- agent: game-designer - agent: game-designer
creates: level-design-doc.md creates: level-design-doc.md
requires: game-design-doc.md requires: game-design-doc.md
optional_steps: optional_steps:
- level_prototyping - level_prototyping
- difficulty_analysis - difficulty_analysis
notes: 'Create level design framework using level-design-doc-tmpl. Establishes content creation guidelines and performance requirements. SAVE OUTPUT: Copy final level-design-doc.md to your project''s docs/design/ folder.' notes: "Create level design framework using level-design-doc-tmpl. Establishes content creation guidelines and performance requirements. SAVE OUTPUT: Copy final level-design-doc.md to your project's docs/design/ folder."
- agent: solution-architect - agent: solution-architect
creates: game-architecture.md creates: game-architecture.md
requires: requires:
@ -41,7 +41,7 @@ workflow:
- technical_research_prompt - technical_research_prompt
- performance_analysis - performance_analysis
- platform_research - platform_research
notes: 'Create comprehensive technical architecture using game-architecture-tmpl. Defines Phaser 3 systems, performance optimization, and code structure. SAVE OUTPUT: Copy final game-architecture.md to your project''s docs/architecture/ folder.' notes: "Create comprehensive technical architecture using game-architecture-tmpl. Defines Phaser 3 systems, performance optimization, and code structure. SAVE OUTPUT: Copy final game-architecture.md to your project's docs/architecture/ folder."
- agent: game-designer - agent: game-designer
validates: design_consistency validates: design_consistency
requires: all_design_documents requires: all_design_documents
@ -66,7 +66,7 @@ workflow:
optional_steps: optional_steps:
- quick_brainstorming - quick_brainstorming
- concept_validation - concept_validation
notes: 'Create focused game brief for prototype. Emphasize core mechanics and immediate playability. SAVE OUTPUT: Copy final game-brief.md to your project''s docs/ folder.' notes: "Create focused game brief for prototype. Emphasize core mechanics and immediate playability. SAVE OUTPUT: Copy final game-brief.md to your project's docs/ folder."
- agent: game-designer - agent: game-designer
creates: prototype-design.md creates: prototype-design.md
uses: create-doc prototype-design OR create-game-story uses: create-doc prototype-design OR create-game-story

View File

@ -44,7 +44,7 @@ workflow:
notes: Implement stories in priority order. Test frequently and adjust design based on what feels fun. Document discoveries. notes: Implement stories in priority order. Test frequently and adjust design based on what feels fun. Document discoveries.
workflow_end: workflow_end:
action: prototype_evaluation action: prototype_evaluation
notes: 'Prototype complete. Evaluate core mechanics, gather feedback, and decide next steps: iterate, expand, or archive.' notes: "Prototype complete. Evaluate core mechanics, gather feedback, and decide next steps: iterate, expand, or archive."
game_jam_sequence: game_jam_sequence:
- step: jam_concept - step: jam_concept
agent: game-designer agent: game-designer

View File

@ -61,13 +61,13 @@ commands:
- explain: teach me what and why you did whatever you just did in detail so I can learn. Explain to me as if you were training a junior Unity developer. - explain: teach me what and why you did whatever you just did in detail so I can learn. Explain to me as if you were training a junior Unity developer.
- exit: Say goodbye as the Game Developer, and then abandon inhabiting this persona - exit: Say goodbye as the Game Developer, and then abandon inhabiting this persona
develop-story: develop-story:
order-of-execution: "Read (first or next) task→Implement Task and its subtasks→Write tests→Execute validations→Only if ALL pass, then update the task checkbox with [x]→Update story section File List to ensure it lists and new or modified or deleted source file→repeat order-of-execution until complete" order-of-execution: 'Read (first or next) task→Implement Task and its subtasks→Write tests→Execute validations→Only if ALL pass, then update the task checkbox with [x]→Update story section File List to ensure it lists and new or modified or deleted source file→repeat order-of-execution until complete'
story-file-updates-ONLY: story-file-updates-ONLY:
- CRITICAL: ONLY UPDATE THE STORY FILE WITH UPDATES TO SECTIONS INDICATED BELOW. DO NOT MODIFY ANY OTHER SECTIONS. - CRITICAL: ONLY UPDATE THE STORY FILE WITH UPDATES TO SECTIONS INDICATED BELOW. DO NOT MODIFY ANY OTHER SECTIONS.
- CRITICAL: You are ONLY authorized to edit these specific sections of story files - Tasks / Subtasks Checkboxes, Dev Agent Record section and all its subsections, Agent Model Used, Debug Log References, Completion Notes List, File List, Change Log, Status - CRITICAL: You are ONLY authorized to edit these specific sections of story files - Tasks / Subtasks Checkboxes, Dev Agent Record section and all its subsections, Agent Model Used, Debug Log References, Completion Notes List, File List, Change Log, Status
- CRITICAL: DO NOT modify Status, Story, Acceptance Criteria, Dev Notes, Testing sections, or any other sections not listed above - CRITICAL: DO NOT modify Status, Story, Acceptance Criteria, Dev Notes, Testing sections, or any other sections not listed above
blocking: "HALT for: Unapproved deps needed, confirm with user | Ambiguous after story check | 3 failures attempting to implement or fix something repeatedly | Missing config | Failing regression" blocking: 'HALT for: Unapproved deps needed, confirm with user | Ambiguous after story check | 3 failures attempting to implement or fix something repeatedly | Missing config | Failing regression'
ready-for-review: "Code matches requirements + All validations pass + Follows Unity & C# standards + File List complete + Stable FPS" ready-for-review: 'Code matches requirements + All validations pass + Follows Unity & C# standards + File List complete + Stable FPS'
completion: "All Tasks and Subtasks marked [x] and have tests→Validations and full regression passes (DON'T BE LAZY, EXECUTE ALL TESTS and CONFIRM)→Ensure File List is Complete→run the task execute-checklist for the checklist game-story-dod-checklist→set story status: 'Ready for Review'→HALT" completion: "All Tasks and Subtasks marked [x] and have tests→Validations and full regression passes (DON'T BE LAZY, EXECUTE ALL TESTS and CONFIRM)→Ensure File List is Complete→run the task execute-checklist for the checklist game-story-dod-checklist→set story status: 'Ready for Review'→HALT"
dependencies: dependencies:
tasks: tasks:

View File

@ -456,7 +456,7 @@ Use the `shard-doc` task or `@kayvan/markdown-tree-parser` tool for automatic ga
- **Claude Code**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect` - **Claude Code**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect`
- **Cursor**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Cursor**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect`
- **Windsurf**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Windsurf**: `/bmad2du/game-designer`, `/bmad2du/game-developer`, `/bmad2du/game-sm`, `/bmad2du/game-architect`
- **Trae**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect` - **Trae**: `@bmad2du/game-designer`, `@bmad2du/game-developer`, `@bmad2du/game-sm`, `@bmad2du/game-architect`
- **Roo Code**: Select mode from mode selector with bmad2du prefix - **Roo Code**: Select mode from mode selector with bmad2du prefix
- **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select the appropriate game agent. - **GitHub Copilot**: Open the Chat view (`⌃⌘I` on Mac, `Ctrl+Alt+I` on Windows/Linux) and select the appropriate game agent.

View File

@ -17,21 +17,21 @@ workflow:
- brainstorming_session - brainstorming_session
- game_research_prompt - game_research_prompt
- player_research - player_research
notes: 'Start with brainstorming game concepts, then create comprehensive game brief. SAVE OUTPUT: Copy final game-brief.md to your project''s docs/design/ folder.' notes: "Start with brainstorming game concepts, then create comprehensive game brief. SAVE OUTPUT: Copy final game-brief.md to your project's docs/design/ folder."
- agent: game-designer - agent: game-designer
creates: game-design-doc.md creates: game-design-doc.md
requires: game-brief.md requires: game-brief.md
optional_steps: optional_steps:
- competitive_analysis - competitive_analysis
- technical_research - technical_research
notes: 'Create detailed Game Design Document using game-design-doc-tmpl. Defines all gameplay mechanics, progression, and technical requirements. SAVE OUTPUT: Copy final game-design-doc.md to your project''s docs/design/ folder.' notes: "Create detailed Game Design Document using game-design-doc-tmpl. Defines all gameplay mechanics, progression, and technical requirements. SAVE OUTPUT: Copy final game-design-doc.md to your project's docs/design/ folder."
- agent: game-designer - agent: game-designer
creates: level-design-doc.md creates: level-design-doc.md
requires: game-design-doc.md requires: game-design-doc.md
optional_steps: optional_steps:
- level_prototyping - level_prototyping
- difficulty_analysis - difficulty_analysis
notes: 'Create level design framework using level-design-doc-tmpl. Establishes content creation guidelines and performance requirements. SAVE OUTPUT: Copy final level-design-doc.md to your project''s docs/design/ folder.' notes: "Create level design framework using level-design-doc-tmpl. Establishes content creation guidelines and performance requirements. SAVE OUTPUT: Copy final level-design-doc.md to your project's docs/design/ folder."
- agent: solution-architect - agent: solution-architect
creates: game-architecture.md creates: game-architecture.md
requires: requires:
@ -41,7 +41,7 @@ workflow:
- technical_research_prompt - technical_research_prompt
- performance_analysis - performance_analysis
- platform_research - platform_research
notes: 'Create comprehensive technical architecture using game-architecture-tmpl. Defines Unity systems, performance optimization, and code structure. SAVE OUTPUT: Copy final game-architecture.md to your project''s docs/architecture/ folder.' notes: "Create comprehensive technical architecture using game-architecture-tmpl. Defines Unity systems, performance optimization, and code structure. SAVE OUTPUT: Copy final game-architecture.md to your project's docs/architecture/ folder."
- agent: game-designer - agent: game-designer
validates: design_consistency validates: design_consistency
requires: all_design_documents requires: all_design_documents
@ -66,7 +66,7 @@ workflow:
optional_steps: optional_steps:
- quick_brainstorming - quick_brainstorming
- concept_validation - concept_validation
notes: 'Create focused game brief for prototype. Emphasize core mechanics and immediate playability. SAVE OUTPUT: Copy final game-brief.md to your project''s docs/ folder.' notes: "Create focused game brief for prototype. Emphasize core mechanics and immediate playability. SAVE OUTPUT: Copy final game-brief.md to your project's docs/ folder."
- agent: game-designer - agent: game-designer
creates: prototype-design.md creates: prototype-design.md
uses: create-doc prototype-design OR create-game-story uses: create-doc prototype-design OR create-game-story

View File

@ -44,7 +44,7 @@ workflow:
notes: Implement stories in priority order. Test frequently in the Unity Editor and adjust design based on what feels fun. Document discoveries. notes: Implement stories in priority order. Test frequently in the Unity Editor and adjust design based on what feels fun. Document discoveries.
workflow_end: workflow_end:
action: prototype_evaluation action: prototype_evaluation
notes: 'Prototype complete. Evaluate core mechanics, gather feedback, and decide next steps: iterate, expand, or archive.' notes: "Prototype complete. Evaluate core mechanics, gather feedback, and decide next steps: iterate, expand, or archive."
game_jam_sequence: game_jam_sequence:
- step: jam_concept - step: jam_concept
agent: game-designer agent: game-designer

1466
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,23 @@
{ {
"$schema": "https://json.schemastore.org/package.json",
"name": "bmad-method", "name": "bmad-method",
"version": "5.0.0", "version": "5.0.0",
"description": "Breakthrough Method of Agile AI-driven Development", "description": "Breakthrough Method of Agile AI-driven Development",
"keywords": [
"agile",
"ai",
"orchestrator",
"development",
"methodology",
"agents",
"bmad"
],
"repository": {
"type": "git",
"url": "git+https://github.com/bmadcode/BMAD-METHOD.git"
},
"license": "MIT",
"author": "Brian (BMad) Madison",
"main": "tools/cli.js", "main": "tools/cli.js",
"bin": { "bin": {
"bmad": "tools/bmad-npx-wrapper.js", "bmad": "tools/bmad-npx-wrapper.js",
@ -11,27 +27,43 @@
"build": "node tools/cli.js build", "build": "node tools/cli.js build",
"build:agents": "node tools/cli.js build --agents-only", "build:agents": "node tools/cli.js build --agents-only",
"build:teams": "node tools/cli.js build --teams-only", "build:teams": "node tools/cli.js build --teams-only",
"list:agents": "node tools/cli.js list:agents",
"validate": "node tools/cli.js validate",
"flatten": "node tools/flattener/main.js", "flatten": "node tools/flattener/main.js",
"format": "prettier --write \"**/*.{js,cjs,mjs,json,md,yml,yaml}\"",
"format:check": "prettier --check \"**/*.{js,cjs,mjs,json,md,yml,yaml}\"",
"install:bmad": "node tools/installer/bin/bmad.js install", "install:bmad": "node tools/installer/bin/bmad.js install",
"format": "prettier --write \"**/*.md\"", "lint": "eslint . --ext .js,.cjs,.mjs,.yml,.yaml --max-warnings=0",
"version:patch": "node tools/version-bump.js patch", "lint:fix": "eslint . --ext .js,.cjs,.mjs,.yml,.yaml --fix",
"version:minor": "node tools/version-bump.js minor", "list:agents": "node tools/cli.js list:agents",
"version:major": "node tools/version-bump.js major", "prepare": "husky",
"version:expansion": "node tools/bump-expansion-version.js",
"version:expansion:set": "node tools/update-expansion-version.js",
"version:all": "node tools/bump-all-versions.js",
"version:all:minor": "node tools/bump-all-versions.js minor",
"version:all:major": "node tools/bump-all-versions.js major",
"version:all:patch": "node tools/bump-all-versions.js patch",
"version:expansion:all": "node tools/bump-all-versions.js",
"version:expansion:all:minor": "node tools/bump-all-versions.js minor",
"version:expansion:all:major": "node tools/bump-all-versions.js major",
"version:expansion:all:patch": "node tools/bump-all-versions.js patch",
"release": "semantic-release", "release": "semantic-release",
"release:test": "semantic-release --dry-run --no-ci || echo 'Config test complete - authentication errors are expected locally'", "release:test": "semantic-release --dry-run --no-ci || echo 'Config test complete - authentication errors are expected locally'",
"prepare": "husky" "validate": "node tools/cli.js validate",
"version:all": "node tools/bump-all-versions.js",
"version:all:major": "node tools/bump-all-versions.js major",
"version:all:minor": "node tools/bump-all-versions.js minor",
"version:all:patch": "node tools/bump-all-versions.js patch",
"version:expansion": "node tools/bump-expansion-version.js",
"version:expansion:all": "node tools/bump-all-versions.js",
"version:expansion:all:major": "node tools/bump-all-versions.js major",
"version:expansion:all:minor": "node tools/bump-all-versions.js minor",
"version:expansion:all:patch": "node tools/bump-all-versions.js patch",
"version:expansion:set": "node tools/update-expansion-version.js",
"version:major": "node tools/version-bump.js major",
"version:minor": "node tools/version-bump.js minor",
"version:patch": "node tools/version-bump.js patch"
},
"lint-staged": {
"**/*.{js,cjs,mjs}": [
"eslint --fix --max-warnings=0",
"prettier --write"
],
"**/*.{yml,yaml}": [
"eslint --fix",
"prettier --write"
],
"**/*.{json,md}": [
"prettier --write"
]
}, },
"dependencies": { "dependencies": {
"@kayvan/markdown-tree-parser": "^1.5.0", "@kayvan/markdown-tree-parser": "^1.5.0",
@ -46,37 +78,25 @@
"ora": "^5.4.1", "ora": "^5.4.1",
"semver": "^7.6.3" "semver": "^7.6.3"
}, },
"keywords": [
"agile",
"ai",
"orchestrator",
"development",
"methodology",
"agents",
"bmad"
],
"author": "Brian (BMad) Madison",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/bmadcode/BMAD-METHOD.git"
},
"engines": {
"node": ">=20.0.0"
},
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-n": "^17.21.3",
"eslint-plugin-unicorn": "^60.0.0",
"eslint-plugin-yml": "^1.18.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jest": "^30.0.4", "jest": "^30.0.4",
"lint-staged": "^16.1.1", "lint-staged": "^16.1.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.19",
"semantic-release": "^22.0.0", "semantic-release": "^22.0.0",
"yaml-eslint-parser": "^1.2.3",
"yaml-lint": "^1.7.0" "yaml-lint": "^1.7.0"
}, },
"lint-staged": { "engines": {
"**/*.md": [ "node": ">=20.10.0"
"prettier --write"
]
} }
} }

32
prettier.config.mjs Normal file
View File

@ -0,0 +1,32 @@
export default {
$schema: 'https://json.schemastore.org/prettierrc',
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'all',
bracketSpacing: true,
arrowParens: 'always',
endOfLine: 'lf',
proseWrap: 'preserve',
overrides: [
{
files: ['*.md'],
options: { proseWrap: 'preserve' },
},
{
files: ['*.yml', '*.yaml'],
options: { singleQuote: false },
},
{
files: ['*.json', '*.jsonc'],
options: { singleQuote: false },
},
{
files: ['*.cjs'],
options: { parser: 'babel' },
},
],
plugins: ['prettier-plugin-packagejson'],
};

View File

@ -5,16 +5,16 @@
* This file ensures proper execution when run via npx from GitHub * This file ensures proper execution when run via npx from GitHub
*/ */
const { execSync } = require('child_process'); const { execSync } = require('node:child_process');
const path = require('path'); const path = require('node:path');
const fs = require('fs'); const fs = require('node:fs');
// Check if we're running in an npx temporary directory // Check if we're running in an npx temporary directory
const isNpxExecution = __dirname.includes('_npx') || __dirname.includes('.npm'); const isNpxExecution = __dirname.includes('_npx') || __dirname.includes('.npm');
// If running via npx, we need to handle things differently // If running via npx, we need to handle things differently
if (isNpxExecution) { if (isNpxExecution) {
const args = process.argv.slice(2); const arguments_ = process.argv.slice(2);
// Use the installer for all commands // Use the installer for all commands
const bmadScriptPath = path.join(__dirname, 'installer', 'bin', 'bmad.js'); const bmadScriptPath = path.join(__dirname, 'installer', 'bin', 'bmad.js');
@ -26,9 +26,9 @@ if (isNpxExecution) {
} }
try { try {
execSync(`node "${bmadScriptPath}" ${args.join(' ')}`, { execSync(`node "${bmadScriptPath}" ${arguments_.join(' ')}`, {
stdio: 'inherit', stdio: 'inherit',
cwd: path.dirname(__dirname) cwd: path.dirname(__dirname),
}); });
} catch (error) { } catch (error) {
process.exit(error.status || 1); process.exit(error.status || 1);

View File

@ -1,23 +1,23 @@
const fs = require("node:fs").promises; const fs = require('node:fs').promises;
const path = require("node:path"); const path = require('node:path');
const DependencyResolver = require("../lib/dependency-resolver"); const DependencyResolver = require('../lib/dependency-resolver');
const yamlUtils = require("../lib/yaml-utils"); const yamlUtilities = require('../lib/yaml-utils');
class WebBuilder { class WebBuilder {
constructor(options = {}) { constructor(options = {}) {
this.rootDir = options.rootDir || process.cwd(); this.rootDir = options.rootDir || process.cwd();
this.outputDirs = options.outputDirs || [path.join(this.rootDir, "dist")]; this.outputDirs = options.outputDirs || [path.join(this.rootDir, 'dist')];
this.resolver = new DependencyResolver(this.rootDir); this.resolver = new DependencyResolver(this.rootDir);
this.templatePath = path.join( this.templatePath = path.join(
this.rootDir, this.rootDir,
"tools", 'tools',
"md-assets", 'md-assets',
"web-agent-startup-instructions.md" 'web-agent-startup-instructions.md',
); );
} }
parseYaml(content) { parseYaml(content) {
const yaml = require("js-yaml"); const yaml = require('js-yaml');
return yaml.load(content); return yaml.load(content);
} }
@ -42,11 +42,21 @@ class WebBuilder {
generateWebInstructions(bundleType, packName = null) { generateWebInstructions(bundleType, packName = null) {
// Generate dynamic web instructions based on bundle type // Generate dynamic web instructions based on bundle type
const rootExample = packName ? `.${packName}` : '.bmad-core'; const rootExample = packName ? `.${packName}` : '.bmad-core';
const examplePath = packName ? `.${packName}/folder/filename.md` : '.bmad-core/folder/filename.md'; const examplePath = packName
const personasExample = packName ? `.${packName}/personas/analyst.md` : '.bmad-core/personas/analyst.md'; ? `.${packName}/folder/filename.md`
const tasksExample = packName ? `.${packName}/tasks/create-story.md` : '.bmad-core/tasks/create-story.md'; : '.bmad-core/folder/filename.md';
const utilsExample = packName ? `.${packName}/utils/template-format.md` : '.bmad-core/utils/template-format.md'; const personasExample = packName
const tasksRef = packName ? `.${packName}/tasks/create-story.md` : '.bmad-core/tasks/create-story.md'; ? `.${packName}/personas/analyst.md`
: '.bmad-core/personas/analyst.md';
const tasksExample = packName
? `.${packName}/tasks/create-story.md`
: '.bmad-core/tasks/create-story.md';
const utilitiesExample = packName
? `.${packName}/utils/template-format.md`
: '.bmad-core/utils/template-format.md';
const tasksReference = packName
? `.${packName}/tasks/create-story.md`
: '.bmad-core/tasks/create-story.md';
return `# Web Agent Bundle Instructions return `# Web Agent Bundle Instructions
@ -79,8 +89,8 @@ dependencies:
These references map directly to bundle sections: These references map directly to bundle sections:
- \`utils: template-format\` → Look for \`==================== START: ${utilsExample} ====================\` - \`utils: template-format\` → Look for \`==================== START: ${utilitiesExample} ====================\`
- \`tasks: create-story\` → Look for \`==================== START: ${tasksRef} ====================\` - \`tasks: create-story\` → Look for \`==================== START: ${tasksReference} ====================\`
3. **Execution Context**: You are operating in a web environment. All your capabilities and knowledge are contained within this bundle. Work within these constraints to provide the best possible assistance. 3. **Execution Context**: You are operating in a web environment. All your capabilities and knowledge are contained within this bundle. Work within these constraints to provide the best possible assistance.
@ -112,10 +122,10 @@ These references map directly to bundle sections:
// Write to all output directories // Write to all output directories
for (const outputDir of this.outputDirs) { for (const outputDir of this.outputDirs) {
const outputPath = path.join(outputDir, "agents"); const outputPath = path.join(outputDir, 'agents');
await fs.mkdir(outputPath, { recursive: true }); await fs.mkdir(outputPath, { recursive: true });
const outputFile = path.join(outputPath, `${agentId}.txt`); const outputFile = path.join(outputPath, `${agentId}.txt`);
await fs.writeFile(outputFile, bundle, "utf8"); await fs.writeFile(outputFile, bundle, 'utf8');
} }
} }
@ -131,10 +141,10 @@ These references map directly to bundle sections:
// Write to all output directories // Write to all output directories
for (const outputDir of this.outputDirs) { for (const outputDir of this.outputDirs) {
const outputPath = path.join(outputDir, "teams"); const outputPath = path.join(outputDir, 'teams');
await fs.mkdir(outputPath, { recursive: true }); await fs.mkdir(outputPath, { recursive: true });
const outputFile = path.join(outputPath, `${teamId}.txt`); const outputFile = path.join(outputPath, `${teamId}.txt`);
await fs.writeFile(outputFile, bundle, "utf8"); await fs.writeFile(outputFile, bundle, 'utf8');
} }
} }
@ -157,7 +167,7 @@ These references map directly to bundle sections:
sections.push(this.formatSection(resourcePath, resource.content, 'bmad-core')); sections.push(this.formatSection(resourcePath, resource.content, 'bmad-core'));
} }
return sections.join("\n"); return sections.join('\n');
} }
async buildTeamBundle(teamId) { async buildTeamBundle(teamId) {
@ -182,12 +192,12 @@ These references map directly to bundle sections:
sections.push(this.formatSection(resourcePath, resource.content, 'bmad-core')); sections.push(this.formatSection(resourcePath, resource.content, 'bmad-core'));
} }
return sections.join("\n"); return sections.join('\n');
} }
processAgentContent(content) { processAgentContent(content) {
// First, replace content before YAML with the template // First, replace content before YAML with the template
const yamlContent = yamlUtils.extractYamlFromAgent(content); const yamlContent = yamlUtilities.extractYamlFromAgent(content);
if (!yamlContent) return content; if (!yamlContent) return content;
const yamlMatch = content.match(/```ya?ml\n([\s\S]*?)\n```/); const yamlMatch = content.match(/```ya?ml\n([\s\S]*?)\n```/);
@ -198,24 +208,24 @@ These references map directly to bundle sections:
// Parse YAML and remove root and IDE-FILE-RESOLUTION properties // Parse YAML and remove root and IDE-FILE-RESOLUTION properties
try { try {
const yaml = require("js-yaml"); const yaml = require('js-yaml');
const parsed = yaml.load(yamlContent); const parsed = yaml.load(yamlContent);
// Remove the properties if they exist at root level // Remove the properties if they exist at root level
delete parsed.root; delete parsed.root;
delete parsed["IDE-FILE-RESOLUTION"]; delete parsed['IDE-FILE-RESOLUTION'];
delete parsed["REQUEST-RESOLUTION"]; delete parsed['REQUEST-RESOLUTION'];
// Also remove from activation-instructions if they exist // Also remove from activation-instructions if they exist
if (parsed["activation-instructions"] && Array.isArray(parsed["activation-instructions"])) { if (parsed['activation-instructions'] && Array.isArray(parsed['activation-instructions'])) {
parsed["activation-instructions"] = parsed["activation-instructions"].filter( parsed['activation-instructions'] = parsed['activation-instructions'].filter(
(instruction) => { (instruction) => {
return ( return (
typeof instruction === 'string' && typeof instruction === 'string' &&
!instruction.startsWith("IDE-FILE-RESOLUTION:") && !instruction.startsWith('IDE-FILE-RESOLUTION:') &&
!instruction.startsWith("REQUEST-RESOLUTION:") !instruction.startsWith('REQUEST-RESOLUTION:')
); );
} },
); );
} }
@ -223,25 +233,25 @@ These references map directly to bundle sections:
const cleanedYaml = yaml.dump(parsed, { lineWidth: -1 }); const cleanedYaml = yaml.dump(parsed, { lineWidth: -1 });
// Get the agent name from the YAML for the header // Get the agent name from the YAML for the header
const agentName = parsed.agent?.id || "agent"; const agentName = parsed.agent?.id || 'agent';
// Build the new content with just the agent header and YAML // Build the new content with just the agent header and YAML
const newHeader = `# ${agentName}\n\nCRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n`; const newHeader = `# ${agentName}\n\nCRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n`;
const afterYaml = content.substring(yamlEndIndex); const afterYaml = content.slice(Math.max(0, yamlEndIndex));
return newHeader + "```yaml\n" + cleanedYaml.trim() + "\n```" + afterYaml; return newHeader + '```yaml\n' + cleanedYaml.trim() + '\n```' + afterYaml;
} catch (error) { } catch (error) {
console.warn("Failed to process agent YAML:", error.message); console.warn('Failed to process agent YAML:', error.message);
// If parsing fails, return original content // If parsing fails, return original content
return content; return content;
} }
} }
formatSection(path, content, bundleRoot = 'bmad-core') { formatSection(path, content, bundleRoot = 'bmad-core') {
const separator = "===================="; const separator = '====================';
// Process agent content if this is an agent file // Process agent content if this is an agent file
if (path.includes("/agents/")) { if (path.includes('/agents/')) {
content = this.processAgentContent(content); content = this.processAgentContent(content);
} }
@ -252,17 +262,17 @@ These references map directly to bundle sections:
`${separator} START: ${path} ${separator}`, `${separator} START: ${path} ${separator}`,
content.trim(), content.trim(),
`${separator} END: ${path} ${separator}`, `${separator} END: ${path} ${separator}`,
"", '',
].join("\n"); ].join('\n');
} }
replaceRootReferences(content, bundleRoot) { replaceRootReferences(content, bundleRoot) {
// Replace {root} with the appropriate bundle root path // Replace {root} with the appropriate bundle root path
return content.replace(/\{root\}/g, `.${bundleRoot}`); return content.replaceAll('{root}', `.${bundleRoot}`);
} }
async validate() { async validate() {
console.log("Validating agent configurations..."); console.log('Validating agent configurations...');
const agents = await this.resolver.listAgents(); const agents = await this.resolver.listAgents();
for (const agentId of agents) { for (const agentId of agents) {
try { try {
@ -274,7 +284,7 @@ These references map directly to bundle sections:
} }
} }
console.log("\nValidating team configurations..."); console.log('\nValidating team configurations...');
const teams = await this.resolver.listTeams(); const teams = await this.resolver.listTeams();
for (const teamId of teams) { for (const teamId of teams) {
try { try {
@ -299,54 +309,54 @@ These references map directly to bundle sections:
} }
async buildExpansionPack(packName, options = {}) { async buildExpansionPack(packName, options = {}) {
const packDir = path.join(this.rootDir, "expansion-packs", packName); const packDir = path.join(this.rootDir, 'expansion-packs', packName);
const outputDirs = [path.join(this.rootDir, "dist", "expansion-packs", packName)]; const outputDirectories = [path.join(this.rootDir, 'dist', 'expansion-packs', packName)];
// Clean output directories if requested // Clean output directories if requested
if (options.clean !== false) { if (options.clean !== false) {
for (const outputDir of outputDirs) { for (const outputDir of outputDirectories) {
try { try {
await fs.rm(outputDir, { recursive: true, force: true }); await fs.rm(outputDir, { recursive: true, force: true });
} catch (error) { } catch {
// Directory might not exist, that's fine // Directory might not exist, that's fine
} }
} }
} }
// Build individual agents first // Build individual agents first
const agentsDir = path.join(packDir, "agents"); const agentsDir = path.join(packDir, 'agents');
try { try {
const agentFiles = await fs.readdir(agentsDir); const agentFiles = await fs.readdir(agentsDir);
const agentMarkdownFiles = agentFiles.filter((f) => f.endsWith(".md")); const agentMarkdownFiles = agentFiles.filter((f) => f.endsWith('.md'));
if (agentMarkdownFiles.length > 0) { if (agentMarkdownFiles.length > 0) {
console.log(` Building individual agents for ${packName}:`); console.log(` Building individual agents for ${packName}:`);
for (const agentFile of agentMarkdownFiles) { for (const agentFile of agentMarkdownFiles) {
const agentName = agentFile.replace(".md", ""); const agentName = agentFile.replace('.md', '');
console.log(` - ${agentName}`); console.log(` - ${agentName}`);
// Build individual agent bundle // Build individual agent bundle
const bundle = await this.buildExpansionAgentBundle(packName, packDir, agentName); const bundle = await this.buildExpansionAgentBundle(packName, packDir, agentName);
// Write to all output directories // Write to all output directories
for (const outputDir of outputDirs) { for (const outputDir of outputDirectories) {
const agentsOutputDir = path.join(outputDir, "agents"); const agentsOutputDir = path.join(outputDir, 'agents');
await fs.mkdir(agentsOutputDir, { recursive: true }); await fs.mkdir(agentsOutputDir, { recursive: true });
const outputFile = path.join(agentsOutputDir, `${agentName}.txt`); const outputFile = path.join(agentsOutputDir, `${agentName}.txt`);
await fs.writeFile(outputFile, bundle, "utf8"); await fs.writeFile(outputFile, bundle, 'utf8');
} }
} }
} }
} catch (error) { } catch {
console.debug(` No agents directory found for ${packName}`); console.debug(` No agents directory found for ${packName}`);
} }
// Build team bundle // Build team bundle
const agentTeamsDir = path.join(packDir, "agent-teams"); const agentTeamsDir = path.join(packDir, 'agent-teams');
try { try {
const teamFiles = await fs.readdir(agentTeamsDir); const teamFiles = await fs.readdir(agentTeamsDir);
const teamFile = teamFiles.find((f) => f.endsWith(".yaml")); const teamFile = teamFiles.find((f) => f.endsWith('.yaml'));
if (teamFile) { if (teamFile) {
console.log(` Building team bundle for ${packName}`); console.log(` Building team bundle for ${packName}`);
@ -356,17 +366,17 @@ These references map directly to bundle sections:
const bundle = await this.buildExpansionTeamBundle(packName, packDir, teamConfigPath); const bundle = await this.buildExpansionTeamBundle(packName, packDir, teamConfigPath);
// Write to all output directories // Write to all output directories
for (const outputDir of outputDirs) { for (const outputDir of outputDirectories) {
const teamsOutputDir = path.join(outputDir, "teams"); const teamsOutputDir = path.join(outputDir, 'teams');
await fs.mkdir(teamsOutputDir, { recursive: true }); await fs.mkdir(teamsOutputDir, { recursive: true });
const outputFile = path.join(teamsOutputDir, teamFile.replace(".yaml", ".txt")); const outputFile = path.join(teamsOutputDir, teamFile.replace('.yaml', '.txt'));
await fs.writeFile(outputFile, bundle, "utf8"); await fs.writeFile(outputFile, bundle, 'utf8');
console.log(` ✓ Created bundle: ${path.relative(this.rootDir, outputFile)}`); console.log(` ✓ Created bundle: ${path.relative(this.rootDir, outputFile)}`);
} }
} else { } else {
console.warn(` ⚠ No team configuration found in ${packName}/agent-teams/`); console.warn(` ⚠ No team configuration found in ${packName}/agent-teams/`);
} }
} catch (error) { } catch {
console.warn(` ⚠ No agent-teams directory found for ${packName}`); console.warn(` ⚠ No agent-teams directory found for ${packName}`);
} }
} }
@ -376,16 +386,16 @@ These references map directly to bundle sections:
const sections = [template]; const sections = [template];
// Add agent configuration // Add agent configuration
const agentPath = path.join(packDir, "agents", `${agentName}.md`); const agentPath = path.join(packDir, 'agents', `${agentName}.md`);
const agentContent = await fs.readFile(agentPath, "utf8"); const agentContent = await fs.readFile(agentPath, 'utf8');
const agentWebPath = this.convertToWebPath(agentPath, packName); const agentWebPath = this.convertToWebPath(agentPath, packName);
sections.push(this.formatSection(agentWebPath, agentContent, packName)); sections.push(this.formatSection(agentWebPath, agentContent, packName));
// Resolve and add agent dependencies // Resolve and add agent dependencies
const yamlContent = yamlUtils.extractYamlFromAgent(agentContent); const yamlContent = yamlUtilities.extractYamlFromAgent(agentContent);
if (yamlContent) { if (yamlContent) {
try { try {
const yaml = require("js-yaml"); const yaml = require('js-yaml');
const agentConfig = yaml.load(yamlContent); const agentConfig = yaml.load(yamlContent);
if (agentConfig.dependencies) { if (agentConfig.dependencies) {
@ -398,59 +408,43 @@ These references map directly to bundle sections:
// Try expansion pack first // Try expansion pack first
const resourcePath = path.join(packDir, resourceType, resourceName); const resourcePath = path.join(packDir, resourceType, resourceName);
try { try {
const resourceContent = await fs.readFile(resourcePath, "utf8"); const resourceContent = await fs.readFile(resourcePath, 'utf8');
const resourceWebPath = this.convertToWebPath(resourcePath, packName); const resourceWebPath = this.convertToWebPath(resourcePath, packName);
sections.push( sections.push(this.formatSection(resourceWebPath, resourceContent, packName));
this.formatSection(resourceWebPath, resourceContent, packName)
);
found = true; found = true;
} catch (error) { } catch {
// Not in expansion pack, continue // Not in expansion pack, continue
} }
// If not found in expansion pack, try core // If not found in expansion pack, try core
if (!found) { if (!found) {
const corePath = path.join( const corePath = path.join(this.rootDir, 'bmad-core', resourceType, resourceName);
this.rootDir,
"bmad-core",
resourceType,
resourceName
);
try { try {
const coreContent = await fs.readFile(corePath, "utf8"); const coreContent = await fs.readFile(corePath, 'utf8');
const coreWebPath = this.convertToWebPath(corePath, packName); const coreWebPath = this.convertToWebPath(corePath, packName);
sections.push( sections.push(this.formatSection(coreWebPath, coreContent, packName));
this.formatSection(coreWebPath, coreContent, packName)
);
found = true; found = true;
} catch (error) { } catch {
// Not in core either, continue // Not in core either, continue
} }
} }
// If not found in core, try common folder // If not found in core, try common folder
if (!found) { if (!found) {
const commonPath = path.join( const commonPath = path.join(this.rootDir, 'common', resourceType, resourceName);
this.rootDir,
"common",
resourceType,
resourceName
);
try { try {
const commonContent = await fs.readFile(commonPath, "utf8"); const commonContent = await fs.readFile(commonPath, 'utf8');
const commonWebPath = this.convertToWebPath(commonPath, packName); const commonWebPath = this.convertToWebPath(commonPath, packName);
sections.push( sections.push(this.formatSection(commonWebPath, commonContent, packName));
this.formatSection(commonWebPath, commonContent, packName)
);
found = true; found = true;
} catch (error) { } catch {
// Not in common either, continue // Not in common either, continue
} }
} }
if (!found) { if (!found) {
console.warn( console.warn(
` ⚠ Dependency ${resourceType}#${resourceName} not found in expansion pack or core` ` ⚠ Dependency ${resourceType}#${resourceName} not found in expansion pack or core`,
); );
} }
} }
@ -462,7 +456,7 @@ These references map directly to bundle sections:
} }
} }
return sections.join("\n"); return sections.join('\n');
} }
async buildExpansionTeamBundle(packName, packDir, teamConfigPath) { async buildExpansionTeamBundle(packName, packDir, teamConfigPath) {
@ -471,38 +465,38 @@ These references map directly to bundle sections:
const sections = [template]; const sections = [template];
// Add team configuration and parse to get agent list // Add team configuration and parse to get agent list
const teamContent = await fs.readFile(teamConfigPath, "utf8"); const teamContent = await fs.readFile(teamConfigPath, 'utf8');
const teamFileName = path.basename(teamConfigPath, ".yaml"); const teamFileName = path.basename(teamConfigPath, '.yaml');
const teamConfig = this.parseYaml(teamContent); const teamConfig = this.parseYaml(teamContent);
const teamWebPath = this.convertToWebPath(teamConfigPath, packName); const teamWebPath = this.convertToWebPath(teamConfigPath, packName);
sections.push(this.formatSection(teamWebPath, teamContent, packName)); sections.push(this.formatSection(teamWebPath, teamContent, packName));
// Get list of expansion pack agents // Get list of expansion pack agents
const expansionAgents = new Set(); const expansionAgents = new Set();
const agentsDir = path.join(packDir, "agents"); const agentsDir = path.join(packDir, 'agents');
try { try {
const agentFiles = await fs.readdir(agentsDir); const agentFiles = await fs.readdir(agentsDir);
for (const agentFile of agentFiles.filter((f) => f.endsWith(".md"))) { for (const agentFile of agentFiles.filter((f) => f.endsWith('.md'))) {
const agentName = agentFile.replace(".md", ""); const agentName = agentFile.replace('.md', '');
expansionAgents.add(agentName); expansionAgents.add(agentName);
} }
} catch (error) { } catch {
console.warn(` ⚠ No agents directory found in ${packName}`); console.warn(` ⚠ No agents directory found in ${packName}`);
} }
// Build a map of all available expansion pack resources for override checking // Build a map of all available expansion pack resources for override checking
const expansionResources = new Map(); const expansionResources = new Map();
const resourceDirs = ["templates", "tasks", "checklists", "workflows", "data"]; const resourceDirectories = ['templates', 'tasks', 'checklists', 'workflows', 'data'];
for (const resourceDir of resourceDirs) { for (const resourceDir of resourceDirectories) {
const resourcePath = path.join(packDir, resourceDir); const resourcePath = path.join(packDir, resourceDir);
try { try {
const resourceFiles = await fs.readdir(resourcePath); const resourceFiles = await fs.readdir(resourcePath);
for (const resourceFile of resourceFiles.filter( for (const resourceFile of resourceFiles.filter(
(f) => f.endsWith(".md") || f.endsWith(".yaml") (f) => f.endsWith('.md') || f.endsWith('.yaml'),
)) { )) {
expansionResources.set(`${resourceDir}#${resourceFile}`, true); expansionResources.set(`${resourceDir}#${resourceFile}`, true);
} }
} catch (error) { } catch {
// Directory might not exist, that's fine // Directory might not exist, that's fine
} }
} }
@ -511,9 +505,9 @@ These references map directly to bundle sections:
const agentsToProcess = teamConfig.agents || []; const agentsToProcess = teamConfig.agents || [];
// Ensure bmad-orchestrator is always included for teams // Ensure bmad-orchestrator is always included for teams
if (!agentsToProcess.includes("bmad-orchestrator")) { if (!agentsToProcess.includes('bmad-orchestrator')) {
console.warn(` ⚠ Team ${teamFileName} missing bmad-orchestrator, adding automatically`); console.warn(` ⚠ Team ${teamFileName} missing bmad-orchestrator, adding automatically`);
agentsToProcess.unshift("bmad-orchestrator"); agentsToProcess.unshift('bmad-orchestrator');
} }
// Track all dependencies from all agents (deduplicated) // Track all dependencies from all agents (deduplicated)
@ -523,7 +517,7 @@ These references map directly to bundle sections:
if (expansionAgents.has(agentId)) { if (expansionAgents.has(agentId)) {
// Use expansion pack version (override) // Use expansion pack version (override)
const agentPath = path.join(agentsDir, `${agentId}.md`); const agentPath = path.join(agentsDir, `${agentId}.md`);
const agentContent = await fs.readFile(agentPath, "utf8"); const agentContent = await fs.readFile(agentPath, 'utf8');
const expansionAgentWebPath = this.convertToWebPath(agentPath, packName); const expansionAgentWebPath = this.convertToWebPath(agentPath, packName);
sections.push(this.formatSection(expansionAgentWebPath, agentContent, packName)); sections.push(this.formatSection(expansionAgentWebPath, agentContent, packName));
@ -551,13 +545,13 @@ These references map directly to bundle sections:
} else { } else {
// Use core BMad version // Use core BMad version
try { try {
const coreAgentPath = path.join(this.rootDir, "bmad-core", "agents", `${agentId}.md`); const coreAgentPath = path.join(this.rootDir, 'bmad-core', 'agents', `${agentId}.md`);
const coreAgentContent = await fs.readFile(coreAgentPath, "utf8"); const coreAgentContent = await fs.readFile(coreAgentPath, 'utf8');
const coreAgentWebPath = this.convertToWebPath(coreAgentPath, packName); const coreAgentWebPath = this.convertToWebPath(coreAgentPath, packName);
sections.push(this.formatSection(coreAgentWebPath, coreAgentContent, packName)); sections.push(this.formatSection(coreAgentWebPath, coreAgentContent, packName));
// Parse and collect dependencies from core agent // Parse and collect dependencies from core agent
const yamlContent = yamlUtils.extractYamlFromAgent(coreAgentContent, true); const yamlContent = yamlUtilities.extractYamlFromAgent(coreAgentContent, true);
if (yamlContent) { if (yamlContent) {
try { try {
const agentConfig = this.parseYaml(yamlContent); const agentConfig = this.parseYaml(yamlContent);
@ -577,7 +571,7 @@ These references map directly to bundle sections:
console.debug(`Failed to parse agent YAML for ${agentId}:`, error.message); console.debug(`Failed to parse agent YAML for ${agentId}:`, error.message);
} }
} }
} catch (error) { } catch {
console.warn(` ⚠ Agent ${agentId} not found in core or expansion pack`); console.warn(` ⚠ Agent ${agentId} not found in core or expansion pack`);
} }
} }
@ -593,38 +587,38 @@ These references map directly to bundle sections:
// We know it exists in expansion pack, find and load it // We know it exists in expansion pack, find and load it
const expansionPath = path.join(packDir, dep.type, dep.name); const expansionPath = path.join(packDir, dep.type, dep.name);
try { try {
const content = await fs.readFile(expansionPath, "utf8"); const content = await fs.readFile(expansionPath, 'utf8');
const expansionWebPath = this.convertToWebPath(expansionPath, packName); const expansionWebPath = this.convertToWebPath(expansionPath, packName);
sections.push(this.formatSection(expansionWebPath, content, packName)); sections.push(this.formatSection(expansionWebPath, content, packName));
console.log(` ✓ Using expansion override for ${key}`); console.log(` ✓ Using expansion override for ${key}`);
found = true; found = true;
} catch (error) { } catch {
// Try next extension // Try next extension
} }
} }
// If not found in expansion pack (or doesn't exist there), try core // If not found in expansion pack (or doesn't exist there), try core
if (!found) { if (!found) {
const corePath = path.join(this.rootDir, "bmad-core", dep.type, dep.name); const corePath = path.join(this.rootDir, 'bmad-core', dep.type, dep.name);
try { try {
const content = await fs.readFile(corePath, "utf8"); const content = await fs.readFile(corePath, 'utf8');
const coreWebPath = this.convertToWebPath(corePath, packName); const coreWebPath = this.convertToWebPath(corePath, packName);
sections.push(this.formatSection(coreWebPath, content, packName)); sections.push(this.formatSection(coreWebPath, content, packName));
found = true; found = true;
} catch (error) { } catch {
// Not in core either, continue // Not in core either, continue
} }
} }
// If not found in core, try common folder // If not found in core, try common folder
if (!found) { if (!found) {
const commonPath = path.join(this.rootDir, "common", dep.type, dep.name); const commonPath = path.join(this.rootDir, 'common', dep.type, dep.name);
try { try {
const content = await fs.readFile(commonPath, "utf8"); const content = await fs.readFile(commonPath, 'utf8');
const commonWebPath = this.convertToWebPath(commonPath, packName); const commonWebPath = this.convertToWebPath(commonPath, packName);
sections.push(this.formatSection(commonWebPath, content, packName)); sections.push(this.formatSection(commonWebPath, content, packName));
found = true; found = true;
} catch (error) { } catch {
// Not in common either, continue // Not in common either, continue
} }
} }
@ -635,16 +629,16 @@ These references map directly to bundle sections:
} }
// Add remaining expansion pack resources not already included as dependencies // Add remaining expansion pack resources not already included as dependencies
for (const resourceDir of resourceDirs) { for (const resourceDir of resourceDirectories) {
const resourcePath = path.join(packDir, resourceDir); const resourcePath = path.join(packDir, resourceDir);
try { try {
const resourceFiles = await fs.readdir(resourcePath); const resourceFiles = await fs.readdir(resourcePath);
for (const resourceFile of resourceFiles.filter( for (const resourceFile of resourceFiles.filter(
(f) => f.endsWith(".md") || f.endsWith(".yaml") (f) => f.endsWith('.md') || f.endsWith('.yaml'),
)) { )) {
const filePath = path.join(resourcePath, resourceFile); const filePath = path.join(resourcePath, resourceFile);
const fileContent = await fs.readFile(filePath, "utf8"); const fileContent = await fs.readFile(filePath, 'utf8');
const fileName = resourceFile.replace(/\.(md|yaml)$/, ""); const fileName = resourceFile.replace(/\.(md|yaml)$/, '');
// Only add if not already included as a dependency // Only add if not already included as a dependency
const resourceKey = `${resourceDir}#${fileName}`; const resourceKey = `${resourceDir}#${fileName}`;
@ -654,21 +648,21 @@ These references map directly to bundle sections:
sections.push(this.formatSection(resourceWebPath, fileContent, packName)); sections.push(this.formatSection(resourceWebPath, fileContent, packName));
} }
} }
} catch (error) { } catch {
// Directory might not exist, that's fine // Directory might not exist, that's fine
} }
} }
return sections.join("\n"); return sections.join('\n');
} }
async listExpansionPacks() { async listExpansionPacks() {
const expansionPacksDir = path.join(this.rootDir, "expansion-packs"); const expansionPacksDir = path.join(this.rootDir, 'expansion-packs');
try { try {
const entries = await fs.readdir(expansionPacksDir, { withFileTypes: true }); const entries = await fs.readdir(expansionPacksDir, { withFileTypes: true });
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name); return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
} catch (error) { } catch {
console.warn("No expansion-packs directory found"); console.warn('No expansion-packs directory found');
return []; return [];
} }
} }

View File

@ -1,11 +1,9 @@
#!/usr/bin/env node const fs = require('node:fs');
const path = require('node:path');
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const args = process.argv.slice(2); const arguments_ = process.argv.slice(2);
const bumpType = args[0] || 'minor'; // default to minor const bumpType = arguments_[0] || 'minor'; // default to minor
if (!['major', 'minor', 'patch'].includes(bumpType)) { if (!['major', 'minor', 'patch'].includes(bumpType)) {
console.log('Usage: node bump-all-versions.js [major|minor|patch]'); console.log('Usage: node bump-all-versions.js [major|minor|patch]');
@ -17,15 +15,19 @@ function bumpVersion(currentVersion, type) {
const [major, minor, patch] = currentVersion.split('.').map(Number); const [major, minor, patch] = currentVersion.split('.').map(Number);
switch (type) { switch (type) {
case 'major': case 'major': {
return `${major + 1}.0.0`; return `${major + 1}.0.0`;
case 'minor': }
case 'minor': {
return `${major}.${minor + 1}.0`; return `${major}.${minor + 1}.0`;
case 'patch': }
case 'patch': {
return `${major}.${minor}.${patch + 1}`; return `${major}.${minor}.${patch + 1}`;
default: }
default: {
return currentVersion; return currentVersion;
} }
}
} }
async function bumpAllVersions() { async function bumpAllVersions() {
@ -43,7 +45,12 @@ async function bumpAllVersions() {
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n'); fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n');
updatedItems.push({ type: 'core', name: 'BMad Core', oldVersion: oldCoreVersion, newVersion: newCoreVersion }); updatedItems.push({
type: 'core',
name: 'BMad Core',
oldVersion: oldCoreVersion,
newVersion: newCoreVersion,
});
console.log(`✓ BMad Core (package.json): ${oldCoreVersion}${newCoreVersion}`); console.log(`✓ BMad Core (package.json): ${oldCoreVersion}${newCoreVersion}`);
} catch (error) { } catch (error) {
console.error(`✗ Failed to update BMad Core: ${error.message}`); console.error(`✗ Failed to update BMad Core: ${error.message}`);
@ -74,7 +81,6 @@ async function bumpAllVersions() {
updatedItems.push({ type: 'expansion', name: packId, oldVersion, newVersion }); updatedItems.push({ type: 'expansion', name: packId, oldVersion, newVersion });
console.log(`${packId}: ${oldVersion}${newVersion}`); console.log(`${packId}: ${oldVersion}${newVersion}`);
} catch (error) { } catch (error) {
console.error(`✗ Failed to update ${packId}: ${error.message}`); console.error(`✗ Failed to update ${packId}: ${error.message}`);
} }
@ -83,20 +89,23 @@ async function bumpAllVersions() {
} }
if (updatedItems.length > 0) { if (updatedItems.length > 0) {
const coreCount = updatedItems.filter(i => i.type === 'core').length; const coreCount = updatedItems.filter((index) => index.type === 'core').length;
const expansionCount = updatedItems.filter(i => i.type === 'expansion').length; const expansionCount = updatedItems.filter((index) => index.type === 'expansion').length;
console.log(`\n✓ Successfully bumped ${updatedItems.length} item(s) with ${bumpType} version bump`); console.log(
`\n✓ Successfully bumped ${updatedItems.length} item(s) with ${bumpType} version bump`,
);
if (coreCount > 0) console.log(` - ${coreCount} core`); if (coreCount > 0) console.log(` - ${coreCount} core`);
if (expansionCount > 0) console.log(` - ${expansionCount} expansion pack(s)`); if (expansionCount > 0) console.log(` - ${expansionCount} expansion pack(s)`);
console.log('\nNext steps:'); console.log('\nNext steps:');
console.log('1. Test the changes'); console.log('1. Test the changes');
console.log('2. Commit: git add -A && git commit -m "chore: bump all versions (' + bumpType + ')"'); console.log(
'2. Commit: git add -A && git commit -m "chore: bump all versions (' + bumpType + ')"',
);
} else { } else {
console.log('No items found to update'); console.log('No items found to update');
} }
} catch (error) { } catch (error) {
console.error('Error reading expansion packs directory:', error.message); console.error('Error reading expansion packs directory:', error.message);
process.exit(1); process.exit(1);

View File

@ -1,17 +1,15 @@
#!/usr/bin/env node
// Load required modules // Load required modules
const fs = require('fs'); const fs = require('node:fs');
const path = require('path'); const path = require('node:path');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
// Parse CLI arguments // Parse CLI arguments
const args = process.argv.slice(2); const arguments_ = process.argv.slice(2);
const packId = args[0]; const packId = arguments_[0];
const bumpType = args[1] || 'minor'; const bumpType = arguments_[1] || 'minor';
// Validate arguments // Validate arguments
if (!packId || args.length > 2) { if (!packId || arguments_.length > 2) {
console.log('Usage: node bump-expansion-version.js <expansion-pack-id> [major|minor|patch]'); console.log('Usage: node bump-expansion-version.js <expansion-pack-id> [major|minor|patch]');
console.log('Default: minor'); console.log('Default: minor');
console.log('Example: node bump-expansion-version.js bmad-creator-tools patch'); console.log('Example: node bump-expansion-version.js bmad-creator-tools patch');
@ -28,10 +26,18 @@ function bumpVersion(currentVersion, type) {
const [major, minor, patch] = currentVersion.split('.').map(Number); const [major, minor, patch] = currentVersion.split('.').map(Number);
switch (type) { switch (type) {
case 'major': return `${major + 1}.0.0`; case 'major': {
case 'minor': return `${major}.${minor + 1}.0`; return `${major + 1}.0.0`;
case 'patch': return `${major}.${minor}.${patch + 1}`; }
default: return currentVersion; case 'minor': {
return `${major}.${minor + 1}.0`;
}
case 'patch': {
return `${major}.${minor}.${patch + 1}`;
}
default: {
return currentVersion;
}
} }
} }
@ -47,11 +53,11 @@ async function updateVersion() {
const packsDir = path.join(__dirname, '..', 'expansion-packs'); const packsDir = path.join(__dirname, '..', 'expansion-packs');
const entries = fs.readdirSync(packsDir, { withFileTypes: true }); const entries = fs.readdirSync(packsDir, { withFileTypes: true });
entries.forEach(entry => { for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('.')) { if (entry.isDirectory() && !entry.name.startsWith('.')) {
console.log(` - ${entry.name}`); console.log(` - ${entry.name}`);
} }
}); }
process.exit(1); process.exit(1);
} }
@ -72,8 +78,9 @@ async function updateVersion() {
console.log(`\n✓ Successfully bumped ${packId} with ${bumpType} version bump`); console.log(`\n✓ Successfully bumped ${packId} with ${bumpType} version bump`);
console.log('\nNext steps:'); console.log('\nNext steps:');
console.log(`1. Test the changes`); console.log(`1. Test the changes`);
console.log(`2. Commit: git add -A && git commit -m "chore: bump ${packId} version (${bumpType})"`); console.log(
`2. Commit: git add -A && git commit -m "chore: bump ${packId} version (${bumpType})"`,
);
} catch (error) { } catch (error) {
console.error('Error updating version:', error.message); console.error('Error updating version:', error.message);
process.exit(1); process.exit(1);

View File

@ -1,10 +1,8 @@
#!/usr/bin/env node
const { Command } = require('commander'); const { Command } = require('commander');
const WebBuilder = require('./builders/web-builder'); const WebBuilder = require('./builders/web-builder');
const V3ToV4Upgrader = require('./upgraders/v3-to-v4-upgrader'); const V3ToV4Upgrader = require('./upgraders/v3-to-v4-upgrader');
const IdeSetup = require('./installer/lib/ide-setup'); const IdeSetup = require('./installer/lib/ide-setup');
const path = require('path'); const path = require('node:path');
const program = new Command(); const program = new Command();
@ -23,7 +21,7 @@ program
.option('--no-clean', 'Skip cleaning output directories') .option('--no-clean', 'Skip cleaning output directories')
.action(async (options) => { .action(async (options) => {
const builder = new WebBuilder({ const builder = new WebBuilder({
rootDir: process.cwd() rootDir: process.cwd(),
}); });
try { try {
@ -66,7 +64,7 @@ program
.option('--no-clean', 'Skip cleaning output directories') .option('--no-clean', 'Skip cleaning output directories')
.action(async (options) => { .action(async (options) => {
const builder = new WebBuilder({ const builder = new WebBuilder({
rootDir: process.cwd() rootDir: process.cwd(),
}); });
try { try {
@ -92,7 +90,7 @@ program
const builder = new WebBuilder({ rootDir: process.cwd() }); const builder = new WebBuilder({ rootDir: process.cwd() });
const agents = await builder.resolver.listAgents(); const agents = await builder.resolver.listAgents();
console.log('Available agents:'); console.log('Available agents:');
agents.forEach(agent => console.log(` - ${agent}`)); for (const agent of agents) console.log(` - ${agent}`);
process.exit(0); process.exit(0);
}); });
@ -103,7 +101,7 @@ program
const builder = new WebBuilder({ rootDir: process.cwd() }); const builder = new WebBuilder({ rootDir: process.cwd() });
const expansions = await builder.listExpansionPacks(); const expansions = await builder.listExpansionPacks();
console.log('Available expansion packs:'); console.log('Available expansion packs:');
expansions.forEach(expansion => console.log(` - ${expansion}`)); for (const expansion of expansions) console.log(` - ${expansion}`);
process.exit(0); process.exit(0);
}); });
@ -147,7 +145,7 @@ program
await upgrader.upgrade({ await upgrader.upgrade({
projectPath: options.project, projectPath: options.project,
dryRun: options.dryRun, dryRun: options.dryRun,
backup: options.backup backup: options.backup,
}); });
}); });

View File

@ -1,7 +1,7 @@
const fs = require("fs-extra"); const fs = require('fs-extra');
const path = require("node:path"); const path = require('node:path');
const os = require("node:os"); const os = require('node:os');
const { isBinaryFile } = require("./binary.js"); const { isBinaryFile } = require('./binary.js');
/** /**
* Aggregate file contents with bounded concurrency. * Aggregate file contents with bounded concurrency.
@ -22,7 +22,7 @@ async function aggregateFileContents(files, rootDir, spinner = null) {
// Automatic concurrency selection based on CPU count and workload size. // Automatic concurrency selection based on CPU count and workload size.
// - Base on 2x logical CPUs, clamped to [2, 64] // - Base on 2x logical CPUs, clamped to [2, 64]
// - For very small workloads, avoid excessive parallelism // - For very small workloads, avoid excessive parallelism
const cpuCount = (os.cpus && Array.isArray(os.cpus()) ? os.cpus().length : (os.cpus?.length || 4)); const cpuCount = os.cpus && Array.isArray(os.cpus()) ? os.cpus().length : os.cpus?.length || 4;
let concurrency = Math.min(64, Math.max(2, (Number(cpuCount) || 4) * 2)); let concurrency = Math.min(64, Math.max(2, (Number(cpuCount) || 4) * 2));
if (files.length > 0 && files.length < concurrency) { if (files.length > 0 && files.length < concurrency) {
concurrency = Math.max(1, Math.min(concurrency, Math.ceil(files.length / 2))); concurrency = Math.max(1, Math.min(concurrency, Math.ceil(files.length / 2)));
@ -37,16 +37,16 @@ async function aggregateFileContents(files, rootDir, spinner = null) {
const binary = await isBinaryFile(filePath); const binary = await isBinaryFile(filePath);
if (binary) { if (binary) {
const size = (await fs.stat(filePath)).size; const { size } = await fs.stat(filePath);
results.binaryFiles.push({ path: relativePath, absolutePath: filePath, size }); results.binaryFiles.push({ path: relativePath, absolutePath: filePath, size });
} else { } else {
const content = await fs.readFile(filePath, "utf8"); const content = await fs.readFile(filePath, 'utf8');
results.textFiles.push({ results.textFiles.push({
path: relativePath, path: relativePath,
absolutePath: filePath, absolutePath: filePath,
content, content,
size: content.length, size: content.length,
lines: content.split("\n").length, lines: content.split('\n').length,
}); });
} }
} catch (error) { } catch (error) {
@ -63,8 +63,8 @@ async function aggregateFileContents(files, rootDir, spinner = null) {
} }
} }
for (let i = 0; i < files.length; i += concurrency) { for (let index = 0; index < files.length; index += concurrency) {
const slice = files.slice(i, i + concurrency); const slice = files.slice(index, index + concurrency);
await Promise.all(slice.map(processOne)); await Promise.all(slice.map(processOne));
} }

View File

@ -1,6 +1,6 @@
const fsp = require("node:fs/promises"); const fsp = require('node:fs/promises');
const path = require("node:path"); const path = require('node:path');
const { Buffer } = require("node:buffer"); const { Buffer } = require('node:buffer');
/** /**
* Efficiently determine if a file is binary without reading the whole file. * Efficiently determine if a file is binary without reading the whole file.
@ -13,25 +13,54 @@ async function isBinaryFile(filePath) {
try { try {
const stats = await fsp.stat(filePath); const stats = await fsp.stat(filePath);
if (stats.isDirectory()) { if (stats.isDirectory()) {
throw new Error("EISDIR: illegal operation on a directory"); throw new Error('EISDIR: illegal operation on a directory');
} }
const binaryExtensions = new Set([ const binaryExtensions = new Set([
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".ico", ".svg", '.jpg',
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", '.jpeg',
".zip", ".tar", ".gz", ".rar", ".7z", '.png',
".exe", ".dll", ".so", ".dylib", '.gif',
".mp3", ".mp4", ".avi", ".mov", ".wav", '.bmp',
".ttf", ".otf", ".woff", ".woff2", '.ico',
".bin", ".dat", ".db", ".sqlite", '.svg',
'.pdf',
'.doc',
'.docx',
'.xls',
'.xlsx',
'.ppt',
'.pptx',
'.zip',
'.tar',
'.gz',
'.rar',
'.7z',
'.exe',
'.dll',
'.so',
'.dylib',
'.mp3',
'.mp4',
'.avi',
'.mov',
'.wav',
'.ttf',
'.otf',
'.woff',
'.woff2',
'.bin',
'.dat',
'.db',
'.sqlite',
]); ]);
const ext = path.extname(filePath).toLowerCase(); const extension = path.extname(filePath).toLowerCase();
if (binaryExtensions.has(ext)) return true; if (binaryExtensions.has(extension)) return true;
if (stats.size === 0) return false; if (stats.size === 0) return false;
const sampleSize = Math.min(4096, stats.size); const sampleSize = Math.min(4096, stats.size);
const fd = await fsp.open(filePath, "r"); const fd = await fsp.open(filePath, 'r');
try { try {
const buffer = Buffer.allocUnsafe(sampleSize); const buffer = Buffer.allocUnsafe(sampleSize);
const { bytesRead } = await fd.read(buffer, 0, sampleSize, 0); const { bytesRead } = await fd.read(buffer, 0, sampleSize, 0);
@ -41,9 +70,7 @@ async function isBinaryFile(filePath) {
await fd.close(); await fd.close();
} }
} catch (error) { } catch (error) {
console.warn( console.warn(`Warning: Could not determine if file is binary: ${filePath} - ${error.message}`);
`Warning: Could not determine if file is binary: ${filePath} - ${error.message}`,
);
return false; return false;
} }
} }

View File

@ -1,18 +1,21 @@
const path = require("node:path"); const path = require('node:path');
const { execFile } = require("node:child_process"); const { execFile } = require('node:child_process');
const { promisify } = require("node:util"); const { promisify } = require('node:util');
const { glob } = require("glob"); const { glob } = require('glob');
const { loadIgnore } = require("./ignoreRules.js"); const { loadIgnore } = require('./ignoreRules.js');
const pExecFile = promisify(execFile); const pExecFile = promisify(execFile);
async function isGitRepo(rootDir) { async function isGitRepo(rootDir) {
try { try {
const { stdout } = await pExecFile("git", [ const { stdout } = await pExecFile('git', ['rev-parse', '--is-inside-work-tree'], {
"rev-parse", cwd: rootDir,
"--is-inside-work-tree", });
], { cwd: rootDir }); return (
return String(stdout || "").toString().trim() === "true"; String(stdout || '')
.toString()
.trim() === 'true'
);
} catch { } catch {
return false; return false;
} }
@ -20,12 +23,10 @@ async function isGitRepo(rootDir) {
async function gitListFiles(rootDir) { async function gitListFiles(rootDir) {
try { try {
const { stdout } = await pExecFile("git", [ const { stdout } = await pExecFile('git', ['ls-files', '-co', '--exclude-standard'], {
"ls-files", cwd: rootDir,
"-co", });
"--exclude-standard", return String(stdout || '')
], { cwd: rootDir });
return String(stdout || "")
.split(/\r?\n/) .split(/\r?\n/)
.map((s) => s.trim()) .map((s) => s.trim())
.filter(Boolean); .filter(Boolean);
@ -48,14 +49,14 @@ async function discoverFiles(rootDir, options = {}) {
const { filter } = await loadIgnore(rootDir); const { filter } = await loadIgnore(rootDir);
// Try git first // Try git first
if (preferGit && await isGitRepo(rootDir)) { if (preferGit && (await isGitRepo(rootDir))) {
const relFiles = await gitListFiles(rootDir); const relFiles = await gitListFiles(rootDir);
const filteredRel = relFiles.filter((p) => filter(p)); const filteredRel = relFiles.filter((p) => filter(p));
return filteredRel.map((p) => path.resolve(rootDir, p)); return filteredRel.map((p) => path.resolve(rootDir, p));
} }
// Glob fallback // Glob fallback
const globbed = await glob("**/*", { const globbed = await glob('**/*', {
cwd: rootDir, cwd: rootDir,
nodir: true, nodir: true,
dot: true, dot: true,

View File

@ -1,8 +1,8 @@
const path = require("node:path"); const path = require('node:path');
const discovery = require("./discovery.js"); const discovery = require('./discovery.js');
const ignoreRules = require("./ignoreRules.js"); const ignoreRules = require('./ignoreRules.js');
const { isBinaryFile } = require("./binary.js"); const { isBinaryFile } = require('./binary.js');
const { aggregateFileContents } = require("./aggregate.js"); const { aggregateFileContents } = require('./aggregate.js');
// Backward-compatible signature; delegate to central loader // Backward-compatible signature; delegate to central loader
async function parseGitignore(gitignorePath) { async function parseGitignore(gitignorePath) {
@ -14,7 +14,7 @@ async function discoverFiles(rootDir) {
// Delegate to discovery module which respects .gitignore and defaults // Delegate to discovery module which respects .gitignore and defaults
return await discovery.discoverFiles(rootDir, { preferGit: true }); return await discovery.discoverFiles(rootDir, { preferGit: true });
} catch (error) { } catch (error) {
console.error("Error discovering files:", error.message); console.error('Error discovering files:', error.message);
return []; return [];
} }
} }

View File

@ -1,147 +1,147 @@
const fs = require("fs-extra"); const fs = require('fs-extra');
const path = require("node:path"); const path = require('node:path');
const ignore = require("ignore"); const ignore = require('ignore');
// Central default ignore patterns for discovery and filtering. // Central default ignore patterns for discovery and filtering.
// These complement .gitignore and are applied regardless of VCS presence. // These complement .gitignore and are applied regardless of VCS presence.
const DEFAULT_PATTERNS = [ const DEFAULT_PATTERNS = [
// Project/VCS // Project/VCS
"**/.bmad-core/**", '**/.bmad-core/**',
"**/.git/**", '**/.git/**',
"**/.svn/**", '**/.svn/**',
"**/.hg/**", '**/.hg/**',
"**/.bzr/**", '**/.bzr/**',
// Package/build outputs // Package/build outputs
"**/node_modules/**", '**/node_modules/**',
"**/bower_components/**", '**/bower_components/**',
"**/vendor/**", '**/vendor/**',
"**/packages/**", '**/packages/**',
"**/build/**", '**/build/**',
"**/dist/**", '**/dist/**',
"**/out/**", '**/out/**',
"**/target/**", '**/target/**',
"**/bin/**", '**/bin/**',
"**/obj/**", '**/obj/**',
"**/release/**", '**/release/**',
"**/debug/**", '**/debug/**',
// Environments // Environments
"**/.venv/**", '**/.venv/**',
"**/venv/**", '**/venv/**',
"**/.virtualenv/**", '**/.virtualenv/**',
"**/virtualenv/**", '**/virtualenv/**',
"**/env/**", '**/env/**',
// Logs & coverage // Logs & coverage
"**/*.log", '**/*.log',
"**/npm-debug.log*", '**/npm-debug.log*',
"**/yarn-debug.log*", '**/yarn-debug.log*',
"**/yarn-error.log*", '**/yarn-error.log*',
"**/lerna-debug.log*", '**/lerna-debug.log*',
"**/coverage/**", '**/coverage/**',
"**/.nyc_output/**", '**/.nyc_output/**',
"**/.coverage/**", '**/.coverage/**',
"**/test-results/**", '**/test-results/**',
// Caches & temp // Caches & temp
"**/.cache/**", '**/.cache/**',
"**/.tmp/**", '**/.tmp/**',
"**/.temp/**", '**/.temp/**',
"**/tmp/**", '**/tmp/**',
"**/temp/**", '**/temp/**',
"**/.sass-cache/**", '**/.sass-cache/**',
// IDE/editor // IDE/editor
"**/.vscode/**", '**/.vscode/**',
"**/.idea/**", '**/.idea/**',
"**/*.swp", '**/*.swp',
"**/*.swo", '**/*.swo',
"**/*~", '**/*~',
"**/.project", '**/.project',
"**/.classpath", '**/.classpath',
"**/.settings/**", '**/.settings/**',
"**/*.sublime-project", '**/*.sublime-project',
"**/*.sublime-workspace", '**/*.sublime-workspace',
// Lockfiles // Lockfiles
"**/package-lock.json", '**/package-lock.json',
"**/yarn.lock", '**/yarn.lock',
"**/pnpm-lock.yaml", '**/pnpm-lock.yaml',
"**/composer.lock", '**/composer.lock',
"**/Pipfile.lock", '**/Pipfile.lock',
// Python/Java/compiled artifacts // Python/Java/compiled artifacts
"**/*.pyc", '**/*.pyc',
"**/*.pyo", '**/*.pyo',
"**/*.pyd", '**/*.pyd',
"**/__pycache__/**", '**/__pycache__/**',
"**/*.class", '**/*.class',
"**/*.jar", '**/*.jar',
"**/*.war", '**/*.war',
"**/*.ear", '**/*.ear',
"**/*.o", '**/*.o',
"**/*.so", '**/*.so',
"**/*.dll", '**/*.dll',
"**/*.exe", '**/*.exe',
// System junk // System junk
"**/lib64/**", '**/lib64/**',
"**/.venv/lib64/**", '**/.venv/lib64/**',
"**/venv/lib64/**", '**/venv/lib64/**',
"**/_site/**", '**/_site/**',
"**/.jekyll-cache/**", '**/.jekyll-cache/**',
"**/.jekyll-metadata", '**/.jekyll-metadata',
"**/.DS_Store", '**/.DS_Store',
"**/.DS_Store?", '**/.DS_Store?',
"**/._*", '**/._*',
"**/.Spotlight-V100/**", '**/.Spotlight-V100/**',
"**/.Trashes/**", '**/.Trashes/**',
"**/ehthumbs.db", '**/ehthumbs.db',
"**/Thumbs.db", '**/Thumbs.db',
"**/desktop.ini", '**/desktop.ini',
// XML outputs // XML outputs
"**/flattened-codebase.xml", '**/flattened-codebase.xml',
"**/repomix-output.xml", '**/repomix-output.xml',
// Images, media, fonts, archives, docs, dylibs // Images, media, fonts, archives, docs, dylibs
"**/*.jpg", '**/*.jpg',
"**/*.jpeg", '**/*.jpeg',
"**/*.png", '**/*.png',
"**/*.gif", '**/*.gif',
"**/*.bmp", '**/*.bmp',
"**/*.ico", '**/*.ico',
"**/*.svg", '**/*.svg',
"**/*.pdf", '**/*.pdf',
"**/*.doc", '**/*.doc',
"**/*.docx", '**/*.docx',
"**/*.xls", '**/*.xls',
"**/*.xlsx", '**/*.xlsx',
"**/*.ppt", '**/*.ppt',
"**/*.pptx", '**/*.pptx',
"**/*.zip", '**/*.zip',
"**/*.tar", '**/*.tar',
"**/*.gz", '**/*.gz',
"**/*.rar", '**/*.rar',
"**/*.7z", '**/*.7z',
"**/*.dylib", '**/*.dylib',
"**/*.mp3", '**/*.mp3',
"**/*.mp4", '**/*.mp4',
"**/*.avi", '**/*.avi',
"**/*.mov", '**/*.mov',
"**/*.wav", '**/*.wav',
"**/*.ttf", '**/*.ttf',
"**/*.otf", '**/*.otf',
"**/*.woff", '**/*.woff',
"**/*.woff2", '**/*.woff2',
// Env files // Env files
"**/.env", '**/.env',
"**/.env.*", '**/.env.*',
"**/*.env", '**/*.env',
// Misc // Misc
"**/junit.xml", '**/junit.xml',
]; ];
async function readIgnoreFile(filePath) { async function readIgnoreFile(filePath) {
try { try {
if (!await fs.pathExists(filePath)) return []; if (!(await fs.pathExists(filePath))) return [];
const content = await fs.readFile(filePath, "utf8"); const content = await fs.readFile(filePath, 'utf8');
return content return content
.split("\n") .split('\n')
.map((l) => l.trim()) .map((l) => l.trim())
.filter((l) => l && !l.startsWith("#")); .filter((l) => l && !l.startsWith('#'));
} catch (err) { } catch {
return []; return [];
} }
} }
@ -153,18 +153,18 @@ async function parseGitignore(gitignorePath) {
async function loadIgnore(rootDir, extraPatterns = []) { async function loadIgnore(rootDir, extraPatterns = []) {
const ig = ignore(); const ig = ignore();
const gitignorePath = path.join(rootDir, ".gitignore"); const gitignorePath = path.join(rootDir, '.gitignore');
const patterns = [ const patterns = [
...await readIgnoreFile(gitignorePath), ...(await readIgnoreFile(gitignorePath)),
...DEFAULT_PATTERNS, ...DEFAULT_PATTERNS,
...extraPatterns, ...extraPatterns,
]; ];
// De-duplicate // De-duplicate
const unique = Array.from(new Set(patterns.map((p) => String(p)))); const unique = [...new Set(patterns.map(String))];
ig.add(unique); ig.add(unique);
// Include-only filter: return true if path should be included // Include-only filter: return true if path should be included
const filter = (relativePath) => !ig.ignores(relativePath.replace(/\\/g, "/")); const filter = (relativePath) => !ig.ignores(relativePath.replaceAll('\\', '/'));
return { ig, filter, patterns: unique }; return { ig, filter, patterns: unique };
} }

View File

@ -1,20 +1,14 @@
#!/usr/bin/env node const { Command } = require('commander');
const fs = require('fs-extra');
const { Command } = require("commander"); const path = require('node:path');
const fs = require("fs-extra"); const process = require('node:process');
const path = require("node:path");
const process = require("node:process");
// Modularized components // Modularized components
const { findProjectRoot } = require("./projectRoot.js"); const { findProjectRoot } = require('./projectRoot.js');
const { promptYesNo, promptPath } = require("./prompts.js"); const { promptYesNo, promptPath } = require('./prompts.js');
const { const { discoverFiles, filterFiles, aggregateFileContents } = require('./files.js');
discoverFiles, const { generateXMLOutput } = require('./xml.js');
filterFiles, const { calculateStatistics } = require('./stats.js');
aggregateFileContents,
} = require("./files.js");
const { generateXMLOutput } = require("./xml.js");
const { calculateStatistics } = require("./stats.js");
/** /**
* Recursively discover all files in a directory * Recursively discover all files in a directory
@ -73,30 +67,30 @@ const { calculateStatistics } = require("./stats.js");
const program = new Command(); const program = new Command();
program program
.name("bmad-flatten") .name('bmad-flatten')
.description("BMad-Method codebase flattener tool") .description('BMad-Method codebase flattener tool')
.version("1.0.0") .version('1.0.0')
.option("-i, --input <path>", "Input directory to flatten", process.cwd()) .option('-i, --input <path>', 'Input directory to flatten', process.cwd())
.option("-o, --output <path>", "Output file path", "flattened-codebase.xml") .option('-o, --output <path>', 'Output file path', 'flattened-codebase.xml')
.action(async (options) => { .action(async (options) => {
let inputDir = path.resolve(options.input); let inputDir = path.resolve(options.input);
let outputPath = path.resolve(options.output); let outputPath = path.resolve(options.output);
// Detect if user explicitly provided -i/--input or -o/--output // Detect if user explicitly provided -i/--input or -o/--output
const argv = process.argv.slice(2); const argv = process.argv.slice(2);
const userSpecifiedInput = argv.some((a) => const userSpecifiedInput = argv.some(
a === "-i" || a === "--input" || a.startsWith("--input=") (a) => a === '-i' || a === '--input' || a.startsWith('--input='),
); );
const userSpecifiedOutput = argv.some((a) => const userSpecifiedOutput = argv.some(
a === "-o" || a === "--output" || a.startsWith("--output=") (a) => a === '-o' || a === '--output' || a.startsWith('--output='),
); );
const noPathArgs = !userSpecifiedInput && !userSpecifiedOutput; const noPathArguments = !userSpecifiedInput && !userSpecifiedOutput;
if (noPathArgs) { if (noPathArguments) {
const detectedRoot = await findProjectRoot(process.cwd()); const detectedRoot = await findProjectRoot(process.cwd());
const suggestedOutput = detectedRoot const suggestedOutput = detectedRoot
? path.join(detectedRoot, "flattened-codebase.xml") ? path.join(detectedRoot, 'flattened-codebase.xml')
: path.resolve("flattened-codebase.xml"); : path.resolve('flattened-codebase.xml');
if (detectedRoot) { if (detectedRoot) {
const useDefaults = await promptYesNo( const useDefaults = await promptYesNo(
@ -107,29 +101,23 @@ program
inputDir = detectedRoot; inputDir = detectedRoot;
outputPath = suggestedOutput; outputPath = suggestedOutput;
} else { } else {
inputDir = await promptPath( inputDir = await promptPath('Enter input directory path', process.cwd());
"Enter input directory path",
process.cwd(),
);
outputPath = await promptPath( outputPath = await promptPath(
"Enter output file path", 'Enter output file path',
path.join(inputDir, "flattened-codebase.xml"), path.join(inputDir, 'flattened-codebase.xml'),
); );
} }
} else { } else {
console.log("Could not auto-detect a project root."); console.log('Could not auto-detect a project root.');
inputDir = await promptPath( inputDir = await promptPath('Enter input directory path', process.cwd());
"Enter input directory path",
process.cwd(),
);
outputPath = await promptPath( outputPath = await promptPath(
"Enter output file path", 'Enter output file path',
path.join(inputDir, "flattened-codebase.xml"), path.join(inputDir, 'flattened-codebase.xml'),
); );
} }
} else { } else {
console.error( console.error(
"Could not auto-detect a project root and no arguments were provided. Please specify -i/--input and -o/--output.", 'Could not auto-detect a project root and no arguments were provided. Please specify -i/--input and -o/--output.',
); );
process.exit(1); process.exit(1);
} }
@ -142,25 +130,23 @@ program
try { try {
// Verify input directory exists // Verify input directory exists
if (!await fs.pathExists(inputDir)) { if (!(await fs.pathExists(inputDir))) {
console.error(`❌ Error: Input directory does not exist: ${inputDir}`); console.error(`❌ Error: Input directory does not exist: ${inputDir}`);
process.exit(1); process.exit(1);
} }
// Import ora dynamically // Import ora dynamically
const { default: ora } = await import("ora"); const { default: ora } = await import('ora');
// Start file discovery with spinner // Start file discovery with spinner
const discoverySpinner = ora("🔍 Discovering files...").start(); const discoverySpinner = ora('🔍 Discovering files...').start();
const files = await discoverFiles(inputDir); const files = await discoverFiles(inputDir);
const filteredFiles = await filterFiles(files, inputDir); const filteredFiles = await filterFiles(files, inputDir);
discoverySpinner.succeed( discoverySpinner.succeed(`📁 Found ${filteredFiles.length} files to include`);
`📁 Found ${filteredFiles.length} files to include`,
);
// Process files with progress tracking // Process files with progress tracking
console.log("Reading file contents"); console.log('Reading file contents');
const processingSpinner = ora("📄 Processing files...").start(); const processingSpinner = ora('📄 Processing files...').start();
const aggregatedContent = await aggregateFileContents( const aggregatedContent = await aggregateFileContents(
filteredFiles, filteredFiles,
inputDir, inputDir,
@ -178,34 +164,30 @@ program
} }
// Generate XML output using streaming // Generate XML output using streaming
const xmlSpinner = ora("🔧 Generating XML output...").start(); const xmlSpinner = ora('🔧 Generating XML output...').start();
await generateXMLOutput(aggregatedContent, outputPath); await generateXMLOutput(aggregatedContent, outputPath);
xmlSpinner.succeed("📝 XML generation completed"); xmlSpinner.succeed('📝 XML generation completed');
// Calculate and display statistics // Calculate and display statistics
const outputStats = await fs.stat(outputPath); const outputStats = await fs.stat(outputPath);
const stats = calculateStatistics(aggregatedContent, outputStats.size); const stats = calculateStatistics(aggregatedContent, outputStats.size);
// Display completion summary // Display completion summary
console.log("\n📊 Completion Summary:"); console.log('\n📊 Completion Summary:');
console.log( console.log(
`✅ Successfully processed ${filteredFiles.length} files into ${ `✅ Successfully processed ${filteredFiles.length} files into ${path.basename(outputPath)}`,
path.basename(outputPath)
}`,
); );
console.log(`📁 Output file: ${outputPath}`); console.log(`📁 Output file: ${outputPath}`);
console.log(`📏 Total source size: ${stats.totalSize}`); console.log(`📏 Total source size: ${stats.totalSize}`);
console.log(`📄 Generated XML size: ${stats.xmlSize}`); console.log(`📄 Generated XML size: ${stats.xmlSize}`);
console.log( console.log(`📝 Total lines of code: ${stats.totalLines.toLocaleString()}`);
`📝 Total lines of code: ${stats.totalLines.toLocaleString()}`,
);
console.log(`🔢 Estimated tokens: ${stats.estimatedTokens}`); console.log(`🔢 Estimated tokens: ${stats.estimatedTokens}`);
console.log( console.log(
`📊 File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`, `📊 File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`,
); );
} catch (error) { } catch (error) {
console.error("❌ Critical error:", error.message); console.error('❌ Critical error:', error.message);
console.error("An unexpected error occurred."); console.error('An unexpected error occurred.');
process.exit(1); process.exit(1);
} }
}); });

View File

@ -1,5 +1,5 @@
const fs = require("fs-extra"); const fs = require('fs-extra');
const path = require("node:path"); const path = require('node:path');
/** /**
* Attempt to find the project root by walking up from startDir * Attempt to find the project root by walking up from startDir
@ -12,24 +12,22 @@ async function findProjectRoot(startDir) {
let dir = path.resolve(startDir); let dir = path.resolve(startDir);
const root = path.parse(dir).root; const root = path.parse(dir).root;
const markers = [ const markers = [
".git", '.git',
"package.json", 'package.json',
"pnpm-workspace.yaml", 'pnpm-workspace.yaml',
"yarn.lock", 'yarn.lock',
"pnpm-lock.yaml", 'pnpm-lock.yaml',
"pyproject.toml", 'pyproject.toml',
"requirements.txt", 'requirements.txt',
"go.mod", 'go.mod',
"Cargo.toml", 'Cargo.toml',
"composer.json", 'composer.json',
".hg", '.hg',
".svn", '.svn',
]; ];
while (true) { while (true) {
const exists = await Promise.all( const exists = await Promise.all(markers.map((m) => fs.pathExists(path.join(dir, m))));
markers.map((m) => fs.pathExists(path.join(dir, m))),
);
if (exists.some(Boolean)) { if (exists.some(Boolean)) {
return dir; return dir;
} }

View File

@ -1,11 +1,11 @@
const os = require("node:os"); const os = require('node:os');
const path = require("node:path"); const path = require('node:path');
const readline = require("node:readline"); const readline = require('node:readline');
const process = require("node:process"); const process = require('node:process');
function expandHome(p) { function expandHome(p) {
if (!p) return p; if (!p) return p;
if (p.startsWith("~")) return path.join(os.homedir(), p.slice(1)); if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
return p; return p;
} }
@ -27,16 +27,16 @@ function promptQuestion(question) {
} }
async function promptYesNo(question, defaultYes = true) { async function promptYesNo(question, defaultYes = true) {
const suffix = defaultYes ? " [Y/n] " : " [y/N] "; const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
const ans = (await promptQuestion(`${question}${suffix}`)).trim().toLowerCase(); const ans = (await promptQuestion(`${question}${suffix}`)).trim().toLowerCase();
if (!ans) return defaultYes; if (!ans) return defaultYes;
if (["y", "yes"].includes(ans)) return true; if (['y', 'yes'].includes(ans)) return true;
if (["n", "no"].includes(ans)) return false; if (['n', 'no'].includes(ans)) return false;
return promptYesNo(question, defaultYes); return promptYesNo(question, defaultYes);
} }
async function promptPath(question, defaultValue) { async function promptPath(question, defaultValue) {
const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` : ""}: `; const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` : ''}: `;
const ans = (await promptQuestion(prompt)).trim(); const ans = (await promptQuestion(prompt)).trim();
return expandHome(ans || defaultValue); return expandHome(ans || defaultValue);
} }

View File

@ -1,49 +1,44 @@
const fs = require("fs-extra"); const fs = require('fs-extra');
function escapeXml(str) { function escapeXml(string_) {
if (typeof str !== "string") { if (typeof string_ !== 'string') {
return String(str); return String(string_);
} }
return str return string_.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll("'", '&apos;');
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/'/g, "&apos;");
} }
function indentFileContent(content) { function indentFileContent(content) {
if (typeof content !== "string") { if (typeof content !== 'string') {
return String(content); return String(content);
} }
return content.split("\n").map((line) => ` ${line}`); return content.split('\n').map((line) => ` ${line}`);
} }
function generateXMLOutput(aggregatedContent, outputPath) { function generateXMLOutput(aggregatedContent, outputPath) {
const { textFiles } = aggregatedContent; const { textFiles } = aggregatedContent;
const writeStream = fs.createWriteStream(outputPath, { encoding: "utf8" }); const writeStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
writeStream.on("error", reject); writeStream.on('error', reject);
writeStream.on("finish", resolve); writeStream.on('finish', resolve);
writeStream.write('<?xml version="1.0" encoding="UTF-8"?>\n'); writeStream.write('<?xml version="1.0" encoding="UTF-8"?>\n');
writeStream.write("<files>\n"); writeStream.write('<files>\n');
// Sort files by path for deterministic order // Sort files by path for deterministic order
const filesSorted = [...textFiles].sort((a, b) => const filesSorted = [...textFiles].sort((a, b) => a.path.localeCompare(b.path));
a.path.localeCompare(b.path)
);
let index = 0; let index = 0;
const writeNext = () => { const writeNext = () => {
if (index >= filesSorted.length) { if (index >= filesSorted.length) {
writeStream.write("</files>\n"); writeStream.write('</files>\n');
writeStream.end(); writeStream.end();
return; return;
} }
const file = filesSorted[index++]; const file = filesSorted[index++];
const p = escapeXml(file.path); const p = escapeXml(file.path);
const content = typeof file.content === "string" ? file.content : ""; const content = typeof file.content === 'string' ? file.content : '';
if (content.length === 0) { if (content.length === 0) {
writeStream.write(`\t<file path='${p}'/>\n`); writeStream.write(`\t<file path='${p}'/>\n`);
@ -51,27 +46,34 @@ function generateXMLOutput(aggregatedContent, outputPath) {
return; return;
} }
const needsCdata = content.includes("<") || content.includes("&") || const needsCdata = content.includes('<') || content.includes('&') || content.includes(']]>');
content.includes("]]>");
if (needsCdata) { if (needsCdata) {
// Open tag and CDATA on their own line with tab indent; content lines indented with two tabs // Open tag and CDATA on their own line with tab indent; content lines indented with two tabs
writeStream.write(`\t<file path='${p}'><![CDATA[\n`); writeStream.write(`\t<file path='${p}'><![CDATA[\n`);
// Safely split any occurrences of "]]>" inside content, trim trailing newlines, indent each line with two tabs // Safely split any occurrences of "]]>" inside content, trim trailing newlines, indent each line with two tabs
const safe = content.replace(/]]>/g, "]]]]><![CDATA[>"); const safe = content.replaceAll(']]>', ']]]]><![CDATA[>');
const trimmed = safe.replace(/[\r\n]+$/, ""); const trimmed = safe.replace(/[\r\n]+$/, '');
const indented = trimmed.length > 0 const indented =
? trimmed.split("\n").map((line) => `\t\t${line}`).join("\n") trimmed.length > 0
: ""; ? trimmed
.split('\n')
.map((line) => `\t\t${line}`)
.join('\n')
: '';
writeStream.write(indented); writeStream.write(indented);
// Close CDATA and attach closing tag directly after the last content line // Close CDATA and attach closing tag directly after the last content line
writeStream.write("]]></file>\n"); writeStream.write(']]></file>\n');
} else { } else {
// Write opening tag then newline; indent content with two tabs; attach closing tag directly after last content char // Write opening tag then newline; indent content with two tabs; attach closing tag directly after last content char
writeStream.write(`\t<file path='${p}'>\n`); writeStream.write(`\t<file path='${p}'>\n`);
const trimmed = content.replace(/[\r\n]+$/, ""); const trimmed = content.replace(/[\r\n]+$/, '');
const indented = trimmed.length > 0 const indented =
? trimmed.split("\n").map((line) => `\t\t${line}`).join("\n") trimmed.length > 0
: ""; ? trimmed
.split('\n')
.map((line) => `\t\t${line}`)
.join('\n')
: '';
writeStream.write(indented); writeStream.write(indented);
writeStream.write(`</file>\n`); writeStream.write(`</file>\n`);
} }

View File

@ -1,13 +1,13 @@
#!/usr/bin/env node #!/usr/bin/env node
const { program } = require('commander'); const { program } = require('commander');
const path = require('path'); const path = require('node:path');
const fs = require('fs').promises; const fs = require('node:fs').promises;
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const chalk = require('chalk').default || require('chalk'); const chalk = require('chalk').default || require('chalk');
const inquirer = require('inquirer').default || require('inquirer'); const inquirer = require('inquirer').default || require('inquirer');
const semver = require('semver'); const semver = require('semver');
const https = require('https'); const https = require('node:https');
// Handle both execution contexts (from root via npx or from installer directory) // Handle both execution contexts (from root via npx or from installer directory)
let version; let version;
@ -18,18 +18,20 @@ try {
version = require('../package.json').version; version = require('../package.json').version;
packageName = require('../package.json').name; packageName = require('../package.json').name;
installer = require('../lib/installer'); installer = require('../lib/installer');
} catch (e) { } catch (error) {
// Fall back to root context (when run via npx from GitHub) // Fall back to root context (when run via npx from GitHub)
console.log(`Installer context not found (${e.message}), trying root context...`); console.log(`Installer context not found (${error.message}), trying root context...`);
try { try {
version = require('../../../package.json').version; version = require('../../../package.json').version;
installer = require('../../../tools/installer/lib/installer'); installer = require('../../../tools/installer/lib/installer');
} catch (e2) { } catch (error) {
console.error('Error: Could not load required modules. Please ensure you are running from the correct directory.'); console.error(
'Error: Could not load required modules. Please ensure you are running from the correct directory.',
);
console.error('Debug info:', { console.error('Debug info:', {
__dirname, __dirname,
cwd: process.cwd(), cwd: process.cwd(),
error: e2.message error: error.message,
}); });
process.exit(1); process.exit(1);
} }
@ -45,8 +47,14 @@ program
.option('-f, --full', 'Install complete BMad Method') .option('-f, --full', 'Install complete BMad Method')
.option('-x, --expansion-only', 'Install only expansion packs (no bmad-core)') .option('-x, --expansion-only', 'Install only expansion packs (no bmad-core)')
.option('-d, --directory <path>', 'Installation directory') .option('-d, --directory <path>', 'Installation directory')
.option('-i, --ide <ide...>', 'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, qwen-code, github-copilot, other)') .option(
.option('-e, --expansion-packs <packs...>', 'Install specific expansion packs (can specify multiple)') '-i, --ide <ide...>',
'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, qwen-code, github-copilot, other)',
)
.option(
'-e, --expansion-packs <packs...>',
'Install specific expansion packs (can specify multiple)',
)
.action(async (options) => { .action(async (options) => {
try { try {
if (!options.full && !options.expansionOnly) { if (!options.full && !options.expansionOnly) {
@ -64,8 +72,8 @@ program
const config = { const config = {
installType, installType,
directory: options.directory || '.', directory: options.directory || '.',
ides: (options.ide || []).filter(ide => ide !== 'other'), ides: (options.ide || []).filter((ide) => ide !== 'other'),
expansionPacks: options.expansionPacks || [] expansionPacks: options.expansionPacks || [],
}; };
await installer.install(config); await installer.install(config);
process.exit(0); process.exit(0);
@ -98,7 +106,7 @@ program
console.log('Checking for updates...'); console.log('Checking for updates...');
// Make HTTP request to npm registry for latest version info // Make HTTP request to npm registry for latest version info
const req = https.get(`https://registry.npmjs.org/${packageName}/latest`, res => { const req = https.get(`https://registry.npmjs.org/${packageName}/latest`, (res) => {
// Check for HTTP errors (non-200 status codes) // Check for HTTP errors (non-200 status codes)
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
console.error(chalk.red(`Update check failed: Received status code ${res.statusCode}`)); console.error(chalk.red(`Update check failed: Received status code ${res.statusCode}`));
@ -107,7 +115,7 @@ program
// Accumulate response data chunks // Accumulate response data chunks
let data = ''; let data = '';
res.on('data', chunk => data += chunk); res.on('data', (chunk) => (data += chunk));
// Process complete response // Process complete response
res.on('end', () => { res.on('end', () => {
@ -117,7 +125,9 @@ program
// Compare versions using semver // Compare versions using semver
if (semver.gt(latest, version)) { if (semver.gt(latest, version)) {
console.log(chalk.bold.blue(`⚠️ ${packageName} update available: ${version}${latest}`)); console.log(
chalk.bold.blue(`⚠️ ${packageName} update available: ${version}${latest}`),
);
console.log(chalk.bold.blue('\nInstall latest by running:')); console.log(chalk.bold.blue('\nInstall latest by running:'));
console.log(chalk.bold.magenta(` npm install ${packageName}@latest`)); console.log(chalk.bold.magenta(` npm install ${packageName}@latest`));
console.log(chalk.dim(' or')); console.log(chalk.dim(' or'));
@ -133,12 +143,12 @@ program
}); });
// Handle network/connection errors // Handle network/connection errors
req.on('error', error => { req.on('error', (error) => {
console.error(chalk.red('Update check failed:'), error.message); console.error(chalk.red('Update check failed:'), error.message);
}); });
// Set 30 second timeout to prevent hanging // Set 30 second timeout to prevent hanging
req.setTimeout(30000, () => { req.setTimeout(30_000, () => {
req.destroy(); req.destroy();
console.error(chalk.red('Update check timed out')); console.error(chalk.red('Update check timed out'));
}); });
@ -183,16 +193,17 @@ program
}); });
async function promptInstallation() { async function promptInstallation() {
// Display ASCII logo // Display ASCII logo
console.log(chalk.bold.cyan(` console.log(
chalk.bold.cyan(`
`)); `),
);
console.log(chalk.bold.magenta('🚀 Universal AI Agent Framework for Any Domain')); console.log(chalk.bold.magenta('🚀 Universal AI Agent Framework for Any Domain'));
console.log(chalk.bold.blue(`✨ Installer v${version}\n`)); console.log(chalk.bold.blue(`✨ Installer v${version}\n`));
@ -210,8 +221,8 @@ async function promptInstallation() {
return 'Please enter a valid project path'; return 'Please enter a valid project path';
} }
return true; return true;
} },
} },
]); ]);
answers.directory = directory; answers.directory = directory;
@ -238,7 +249,8 @@ async function promptInstallation() {
if (state.type === 'v4_existing') { if (state.type === 'v4_existing') {
const currentVersion = state.manifest?.version || 'unknown'; const currentVersion = state.manifest?.version || 'unknown';
const newVersion = version; // Always use package.json version const newVersion = version; // Always use package.json version
const versionInfo = currentVersion === newVersion const versionInfo =
currentVersion === newVersion
? `(v${currentVersion} - reinstall)` ? `(v${currentVersion} - reinstall)`
: `(v${currentVersion} → v${newVersion})`; : `(v${currentVersion} → v${newVersion})`;
bmadOptionText = `Update ${coreShortTitle} ${versionInfo} .bmad-core`; bmadOptionText = `Update ${coreShortTitle} ${versionInfo} .bmad-core`;
@ -249,7 +261,7 @@ async function promptInstallation() {
choices.push({ choices.push({
name: bmadOptionText, name: bmadOptionText,
value: 'bmad-core', value: 'bmad-core',
checked: true checked: true,
}); });
// Add expansion pack options // Add expansion pack options
@ -260,7 +272,8 @@ async function promptInstallation() {
if (existing) { if (existing) {
const currentVersion = existing.manifest?.version || 'unknown'; const currentVersion = existing.manifest?.version || 'unknown';
const newVersion = pack.version; const newVersion = pack.version;
const versionInfo = currentVersion === newVersion const versionInfo =
currentVersion === newVersion
? `(v${currentVersion} - reinstall)` ? `(v${currentVersion} - reinstall)`
: `(v${currentVersion} → v${newVersion})`; : `(v${currentVersion} → v${newVersion})`;
packOptionText = `Update ${pack.shortTitle} ${versionInfo} .${pack.id}`; packOptionText = `Update ${pack.shortTitle} ${versionInfo} .${pack.id}`;
@ -271,7 +284,7 @@ async function promptInstallation() {
choices.push({ choices.push({
name: packOptionText, name: packOptionText,
value: pack.id, value: pack.id,
checked: false checked: false,
}); });
} }
@ -287,13 +300,13 @@ async function promptInstallation() {
return 'Please select at least one item to install'; return 'Please select at least one item to install';
} }
return true; return true;
} },
} },
]); ]);
// Process selections // Process selections
answers.installType = selectedItems.includes('bmad-core') ? 'full' : 'expansion-only'; answers.installType = selectedItems.includes('bmad-core') ? 'full' : 'expansion-only';
answers.expansionPacks = selectedItems.filter(item => item !== 'bmad-core'); answers.expansionPacks = selectedItems.filter((item) => item !== 'bmad-core');
// Ask sharding questions if installing BMad core // Ask sharding questions if installing BMad core
if (selectedItems.includes('bmad-core')) { if (selectedItems.includes('bmad-core')) {
@ -306,8 +319,8 @@ async function promptInstallation() {
type: 'confirm', type: 'confirm',
name: 'prdSharded', name: 'prdSharded',
message: 'Will the PRD (Product Requirements Document) be sharded into multiple files?', message: 'Will the PRD (Product Requirements Document) be sharded into multiple files?',
default: true default: true,
} },
]); ]);
answers.prdSharded = prdSharded; answers.prdSharded = prdSharded;
@ -317,18 +330,30 @@ async function promptInstallation() {
type: 'confirm', type: 'confirm',
name: 'architectureSharded', name: 'architectureSharded',
message: 'Will the architecture documentation be sharded into multiple files?', message: 'Will the architecture documentation be sharded into multiple files?',
default: true default: true,
} },
]); ]);
answers.architectureSharded = architectureSharded; answers.architectureSharded = architectureSharded;
// Show warning if architecture sharding is disabled // Show warning if architecture sharding is disabled
if (!architectureSharded) { if (!architectureSharded) {
console.log(chalk.yellow.bold('\n⚠ IMPORTANT: Architecture Sharding Disabled')); console.log(chalk.yellow.bold('\n⚠ IMPORTANT: Architecture Sharding Disabled'));
console.log(chalk.yellow('With architecture sharding disabled, you should still create the files listed')); console.log(
console.log(chalk.yellow('in devLoadAlwaysFiles (like coding-standards.md, tech-stack.md, source-tree.md)')); chalk.yellow(
'With architecture sharding disabled, you should still create the files listed',
),
);
console.log(
chalk.yellow(
'in devLoadAlwaysFiles (like coding-standards.md, tech-stack.md, source-tree.md)',
),
);
console.log(chalk.yellow('as these are used by the dev agent at runtime.')); console.log(chalk.yellow('as these are used by the dev agent at runtime.'));
console.log(chalk.yellow('\nAlternatively, you can remove these files from the devLoadAlwaysFiles list')); console.log(
chalk.yellow(
'\nAlternatively, you can remove these files from the devLoadAlwaysFiles list',
),
);
console.log(chalk.yellow('in your core-config.yaml after installation.')); console.log(chalk.yellow('in your core-config.yaml after installation.'));
const { acknowledge } = await inquirer.prompt([ const { acknowledge } = await inquirer.prompt([
@ -336,8 +361,8 @@ async function promptInstallation() {
type: 'confirm', type: 'confirm',
name: 'acknowledge', name: 'acknowledge',
message: 'Do you acknowledge this requirement and want to proceed?', message: 'Do you acknowledge this requirement and want to proceed?',
default: false default: false,
} },
]); ]);
if (!acknowledge) { if (!acknowledge) {
@ -353,7 +378,11 @@ async function promptInstallation() {
while (!ideSelectionComplete) { while (!ideSelectionComplete) {
console.log(chalk.cyan('\n🛠 IDE Configuration')); console.log(chalk.cyan('\n🛠 IDE Configuration'));
console.log(chalk.bold.yellow.bgRed(' ⚠️ IMPORTANT: This is a MULTISELECT! Use SPACEBAR to toggle each IDE! ')); console.log(
chalk.bold.yellow.bgRed(
' ⚠️ IMPORTANT: This is a MULTISELECT! Use SPACEBAR to toggle each IDE! ',
),
);
console.log(chalk.bold.magenta('🔸 Use arrow keys to navigate')); console.log(chalk.bold.magenta('🔸 Use arrow keys to navigate'));
console.log(chalk.bold.magenta('🔸 Use SPACEBAR to select/deselect IDEs')); console.log(chalk.bold.magenta('🔸 Use SPACEBAR to select/deselect IDEs'));
console.log(chalk.bold.magenta('🔸 Press ENTER when finished selecting\n')); console.log(chalk.bold.magenta('🔸 Press ENTER when finished selecting\n'));
@ -362,7 +391,8 @@ async function promptInstallation() {
{ {
type: 'checkbox', type: 'checkbox',
name: 'ides', name: 'ides',
message: 'Which IDE(s) do you want to configure? (Select with SPACEBAR, confirm with ENTER):', message:
'Which IDE(s) do you want to configure? (Select with SPACEBAR, confirm with ENTER):',
choices: [ choices: [
{ name: 'Cursor', value: 'cursor' }, { name: 'Cursor', value: 'cursor' },
{ name: 'Claude Code', value: 'claude-code' }, { name: 'Claude Code', value: 'claude-code' },
@ -373,9 +403,9 @@ async function promptInstallation() {
{ name: 'Cline', value: 'cline' }, { name: 'Cline', value: 'cline' },
{ name: 'Gemini CLI', value: 'gemini' }, { name: 'Gemini CLI', value: 'gemini' },
{ name: 'Qwen Code', value: 'qwen-code' }, { name: 'Qwen Code', value: 'qwen-code' },
{ name: 'Github Copilot', value: 'github-copilot' } { name: 'Github Copilot', value: 'github-copilot' },
] ],
} },
]); ]);
ides = ideResponse.ides; ides = ideResponse.ides;
@ -386,13 +416,19 @@ async function promptInstallation() {
{ {
type: 'confirm', type: 'confirm',
name: 'confirmNoIde', name: 'confirmNoIde',
message: chalk.red('⚠️ You have NOT selected any IDEs. This means NO IDE integration will be set up. Is this correct?'), message: chalk.red(
default: false '⚠️ You have NOT selected any IDEs. This means NO IDE integration will be set up. Is this correct?',
} ),
default: false,
},
]); ]);
if (!confirmNoIde) { if (!confirmNoIde) {
console.log(chalk.bold.red('\n🔄 Returning to IDE selection. Remember to use SPACEBAR to select IDEs!\n')); console.log(
chalk.bold.red(
'\n🔄 Returning to IDE selection. Remember to use SPACEBAR to select IDEs!\n',
),
);
continue; // Go back to IDE selection only continue; // Go back to IDE selection only
} }
} }
@ -406,7 +442,9 @@ async function promptInstallation() {
// Configure GitHub Copilot immediately if selected // Configure GitHub Copilot immediately if selected
if (ides.includes('github-copilot')) { if (ides.includes('github-copilot')) {
console.log(chalk.cyan('\n🔧 GitHub Copilot Configuration')); console.log(chalk.cyan('\n🔧 GitHub Copilot Configuration'));
console.log(chalk.dim('BMad works best with specific VS Code settings for optimal agent experience.\n')); console.log(
chalk.dim('BMad works best with specific VS Code settings for optimal agent experience.\n'),
);
const { configChoice } = await inquirer.prompt([ const { configChoice } = await inquirer.prompt([
{ {
@ -416,19 +454,19 @@ async function promptInstallation() {
choices: [ choices: [
{ {
name: 'Use recommended defaults (fastest setup)', name: 'Use recommended defaults (fastest setup)',
value: 'defaults' value: 'defaults',
}, },
{ {
name: 'Configure each setting manually (customize to your preferences)', name: 'Configure each setting manually (customize to your preferences)',
value: 'manual' value: 'manual',
}, },
{ {
name: 'Skip settings configuration (I\'ll configure manually later)', name: "Skip settings configuration (I'll configure manually later)",
value: 'skip' value: 'skip',
} },
], ],
default: 'defaults' default: 'defaults',
} },
]); ]);
answers.githubCopilotConfig = { configChoice }; answers.githubCopilotConfig = { configChoice };
@ -439,14 +477,17 @@ async function promptInstallation() {
{ {
type: 'confirm', type: 'confirm',
name: 'includeWebBundles', name: 'includeWebBundles',
message: 'Would you like to include pre-built web bundles? (standalone files for ChatGPT, Claude, Gemini)', message:
default: false 'Would you like to include pre-built web bundles? (standalone files for ChatGPT, Claude, Gemini)',
} default: false,
},
]); ]);
if (includeWebBundles) { if (includeWebBundles) {
console.log(chalk.cyan('\n📦 Web bundles are standalone files perfect for web AI platforms.')); console.log(chalk.cyan('\n📦 Web bundles are standalone files perfect for web AI platforms.'));
console.log(chalk.dim(' You can choose different teams/agents than your IDE installation.\n')); console.log(
chalk.dim(' You can choose different teams/agents than your IDE installation.\n'),
);
const { webBundleType } = await inquirer.prompt([ const { webBundleType } = await inquirer.prompt([
{ {
@ -456,22 +497,22 @@ async function promptInstallation() {
choices: [ choices: [
{ {
name: 'All available bundles (agents, teams, expansion packs)', name: 'All available bundles (agents, teams, expansion packs)',
value: 'all' value: 'all',
}, },
{ {
name: 'Specific teams only', name: 'Specific teams only',
value: 'teams' value: 'teams',
}, },
{ {
name: 'Individual agents only', name: 'Individual agents only',
value: 'agents' value: 'agents',
}, },
{ {
name: 'Custom selection', name: 'Custom selection',
value: 'custom' value: 'custom',
} },
] ],
} },
]); ]);
answers.webBundleType = webBundleType; answers.webBundleType = webBundleType;
@ -484,18 +525,18 @@ async function promptInstallation() {
type: 'checkbox', type: 'checkbox',
name: 'selectedTeams', name: 'selectedTeams',
message: 'Select team bundles to include:', message: 'Select team bundles to include:',
choices: teams.map(t => ({ choices: teams.map((t) => ({
name: `${t.icon || '📋'} ${t.name}: ${t.description}`, name: `${t.icon || '📋'} ${t.name}: ${t.description}`,
value: t.id, value: t.id,
checked: webBundleType === 'teams' // Check all if teams-only mode checked: webBundleType === 'teams', // Check all if teams-only mode
})), })),
validate: (answer) => { validate: (answer) => {
if (answer.length < 1) { if (answer.length === 0) {
return 'You must select at least one team.'; return 'You must select at least one team.';
} }
return true; return true;
} },
} },
]); ]);
answers.selectedWebBundleTeams = selectedTeams; answers.selectedWebBundleTeams = selectedTeams;
} }
@ -507,8 +548,8 @@ async function promptInstallation() {
type: 'confirm', type: 'confirm',
name: 'includeIndividualAgents', name: 'includeIndividualAgents',
message: 'Also include individual agent bundles?', message: 'Also include individual agent bundles?',
default: true default: true,
} },
]); ]);
answers.includeIndividualAgents = includeIndividualAgents; answers.includeIndividualAgents = includeIndividualAgents;
} }
@ -524,8 +565,8 @@ async function promptInstallation() {
return 'Please enter a valid directory path'; return 'Please enter a valid directory path';
} }
return true; return true;
} },
} },
]); ]);
answers.webBundlesDirectory = webBundlesDirectory; answers.webBundlesDirectory = webBundlesDirectory;
} }
@ -538,6 +579,6 @@ async function promptInstallation() {
program.parse(process.argv); program.parse(process.argv);
// Show help if no command provided // Show help if no command provided
if (!process.argv.slice(2).length) { if (process.argv.slice(2).length === 0) {
program.outputHelp(); program.outputHelp();
} }

View File

@ -30,12 +30,12 @@ ide-configurations:
# 2. Claude will switch to that agent's persona # 2. Claude will switch to that agent's persona
windsurf: windsurf:
name: Windsurf name: Windsurf
rule-dir: .windsurf/rules/ rule-dir: .windsurf/workflows/
format: multi-file format: multi-file
command-suffix: .md command-suffix: .md
instructions: | instructions: |
# To use BMad agents in Windsurf: # To use BMad agents in Windsurf:
# 1. Type @agent-name (e.g., "@dev", "@pm") # 1. Type /agent-name (e.g., "/dev", "/pm")
# 2. Windsurf will adopt that agent's persona # 2. Windsurf will adopt that agent's persona
trae: trae:
name: Trae name: Trae

View File

@ -1,5 +1,5 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('node:path');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const { extractYamlFromAgent } = require('../../lib/yaml-utils'); const { extractYamlFromAgent } = require('../../lib/yaml-utils');
@ -51,7 +51,7 @@ class ConfigLoader {
id: agentId, id: agentId,
name: agentConfig.title || agentConfig.name || agentId, name: agentConfig.title || agentConfig.name || agentId,
file: `bmad-core/agents/${entry.name}`, file: `bmad-core/agents/${entry.name}`,
description: agentConfig.whenToUse || 'No description available' description: agentConfig.whenToUse || 'No description available',
}); });
} }
} catch (error) { } catch (error) {
@ -90,21 +90,25 @@ class ConfigLoader {
expansionPacks.push({ expansionPacks.push({
id: entry.name, id: entry.name,
name: config.name || entry.name, name: config.name || entry.name,
description: config['short-title'] || config.description || 'No description available', description:
fullDescription: config.description || config['short-title'] || 'No description available', config['short-title'] || config.description || 'No description available',
fullDescription:
config.description || config['short-title'] || 'No description available',
version: config.version || '1.0.0', version: config.version || '1.0.0',
author: config.author || 'BMad Team', author: config.author || 'BMad Team',
packPath: packPath, packPath: packPath,
dependencies: config.dependencies?.agents || [] dependencies: config.dependencies?.agents || [],
}); });
} catch (error) { } catch (error) {
// Fallback if config.yaml doesn't exist or can't be read // Fallback if config.yaml doesn't exist or can't be read
console.warn(`Failed to read config for expansion pack ${entry.name}: ${error.message}`); console.warn(
`Failed to read config for expansion pack ${entry.name}: ${error.message}`,
);
// Try to derive info from directory name as fallback // Try to derive info from directory name as fallback
const name = entry.name const name = entry.name
.split('-') .split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1)) .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '); .join(' ');
expansionPacks.push({ expansionPacks.push({
@ -115,7 +119,7 @@ class ConfigLoader {
version: '1.0.0', version: '1.0.0',
author: 'BMad Team', author: 'BMad Team',
packPath: packPath, packPath: packPath,
dependencies: [] dependencies: [],
}); });
} }
} }
@ -193,7 +197,7 @@ class ConfigLoader {
id: path.basename(entry.name, '.yaml'), id: path.basename(entry.name, '.yaml'),
name: teamConfig.bundle.name || entry.name, name: teamConfig.bundle.name || entry.name,
description: teamConfig.bundle.description || 'Team configuration', description: teamConfig.bundle.description || 'Team configuration',
icon: teamConfig.bundle.icon || '📋' icon: teamConfig.bundle.icon || '📋',
}); });
} }
} catch (error) { } catch (error) {

View File

@ -1,17 +1,14 @@
const fs = require("fs-extra"); const fs = require('fs-extra');
const path = require("path"); const path = require('node:path');
const crypto = require("crypto"); const crypto = require('node:crypto');
const yaml = require("js-yaml"); const yaml = require('js-yaml');
const chalk = require("chalk").default || require("chalk"); const chalk = require('chalk');
const { createReadStream, createWriteStream, promises: fsPromises } = require('fs'); const { createReadStream, createWriteStream, promises: fsPromises } = require('node:fs');
const { pipeline } = require('stream/promises'); const { pipeline } = require('node:stream/promises');
const resourceLocator = require('./resource-locator'); const resourceLocator = require('./resource-locator');
class FileManager { class FileManager {
constructor() { constructor() {}
this.manifestDir = ".bmad-core";
this.manifestFile = "install-manifest.yaml";
}
async copyFile(source, destination) { async copyFile(source, destination) {
try { try {
@ -19,14 +16,9 @@ class FileManager {
// Use streaming for large files (> 10MB) // Use streaming for large files (> 10MB)
const stats = await fs.stat(source); const stats = await fs.stat(source);
if (stats.size > 10 * 1024 * 1024) { await (stats.size > 10 * 1024 * 1024
await pipeline( ? pipeline(createReadStream(source), createWriteStream(destination))
createReadStream(source), : fs.copy(source, destination));
createWriteStream(destination)
);
} else {
await fs.copy(source, destination);
}
return true; return true;
} catch (error) { } catch (error) {
console.error(chalk.red(`Failed to copy ${source}:`), error.message); console.error(chalk.red(`Failed to copy ${source}:`), error.message);
@ -41,28 +33,20 @@ class FileManager {
// Use streaming copy for large directories // Use streaming copy for large directories
const files = await resourceLocator.findFiles('**/*', { const files = await resourceLocator.findFiles('**/*', {
cwd: source, cwd: source,
nodir: true nodir: true,
}); });
// Process files in batches to avoid memory issues // Process files in batches to avoid memory issues
const batchSize = 50; const batchSize = 50;
for (let i = 0; i < files.length; i += batchSize) { for (let index = 0; index < files.length; index += batchSize) {
const batch = files.slice(i, i + batchSize); const batch = files.slice(index, index + batchSize);
await Promise.all( await Promise.all(
batch.map(file => batch.map((file) => this.copyFile(path.join(source, file), path.join(destination, file))),
this.copyFile(
path.join(source, file),
path.join(destination, file)
)
)
); );
} }
return true; return true;
} catch (error) { } catch (error) {
console.error( console.error(chalk.red(`Failed to copy directory ${source}:`), error.message);
chalk.red(`Failed to copy directory ${source}:`),
error.message
);
return false; return false;
} }
} }
@ -73,17 +57,16 @@ class FileManager {
for (const file of files) { for (const file of files) {
const sourcePath = path.join(sourceDir, file); const sourcePath = path.join(sourceDir, file);
const destPath = path.join(destDir, file); const destinationPath = path.join(destDir, file);
// Use root replacement if rootValue is provided and file needs it // Use root replacement if rootValue is provided and file needs it
const needsRootReplacement = rootValue && (file.endsWith('.md') || file.endsWith('.yaml') || file.endsWith('.yml')); const needsRootReplacement =
rootValue && (file.endsWith('.md') || file.endsWith('.yaml') || file.endsWith('.yml'));
let success = false; let success = false;
if (needsRootReplacement) { success = await (needsRootReplacement
success = await this.copyFileWithRootReplacement(sourcePath, destPath, rootValue); ? this.copyFileWithRootReplacement(sourcePath, destinationPath, rootValue)
} else { : this.copyFile(sourcePath, destinationPath));
success = await this.copyFile(sourcePath, destPath);
}
if (success) { if (success) {
copied.push(file); copied.push(file);
@ -97,32 +80,28 @@ class FileManager {
try { try {
// Use streaming for hash calculation to reduce memory usage // Use streaming for hash calculation to reduce memory usage
const stream = createReadStream(filePath); const stream = createReadStream(filePath);
const hash = crypto.createHash("sha256"); const hash = crypto.createHash('sha256');
for await (const chunk of stream) { for await (const chunk of stream) {
hash.update(chunk); hash.update(chunk);
} }
return hash.digest("hex").slice(0, 16); return hash.digest('hex').slice(0, 16);
} catch (error) { } catch {
return null; return null;
} }
} }
async createManifest(installDir, config, files) { async createManifest(installDir, config, files) {
const manifestPath = path.join( const manifestPath = path.join(installDir, this.manifestDir, this.manifestFile);
installDir,
this.manifestDir,
this.manifestFile
);
// Read version from package.json // Read version from package.json
let coreVersion = "unknown"; let coreVersion = 'unknown';
try { try {
const packagePath = path.join(__dirname, '..', '..', '..', 'package.json'); const packagePath = path.join(__dirname, '..', '..', '..', 'package.json');
const packageJson = require(packagePath); const packageJson = require(packagePath);
coreVersion = packageJson.version; coreVersion = packageJson.version;
} catch (error) { } catch {
console.warn("Could not read version from package.json, using 'unknown'"); console.warn("Could not read version from package.json, using 'unknown'");
} }
@ -156,31 +135,23 @@ class FileManager {
} }
async readManifest(installDir) { async readManifest(installDir) {
const manifestPath = path.join( const manifestPath = path.join(installDir, this.manifestDir, this.manifestFile);
installDir,
this.manifestDir,
this.manifestFile
);
try { try {
const content = await fs.readFile(manifestPath, "utf8"); const content = await fs.readFile(manifestPath, 'utf8');
return yaml.load(content); return yaml.load(content);
} catch (error) { } catch {
return null; return null;
} }
} }
async readExpansionPackManifest(installDir, packId) { async readExpansionPackManifest(installDir, packId) {
const manifestPath = path.join( const manifestPath = path.join(installDir, `.${packId}`, this.manifestFile);
installDir,
`.${packId}`,
this.manifestFile
);
try { try {
const content = await fs.readFile(manifestPath, "utf8"); const content = await fs.readFile(manifestPath, 'utf8');
return yaml.load(content); return yaml.load(content);
} catch (error) { } catch {
return null; return null;
} }
} }
@ -203,7 +174,7 @@ class FileManager {
async checkFileIntegrity(installDir, manifest) { async checkFileIntegrity(installDir, manifest) {
const result = { const result = {
missing: [], missing: [],
modified: [] modified: [],
}; };
for (const file of manifest.files) { for (const file of manifest.files) {
@ -214,13 +185,13 @@ class FileManager {
continue; continue;
} }
if (!(await this.pathExists(filePath))) { if (await this.pathExists(filePath)) {
result.missing.push(file.path);
} else {
const currentHash = await this.calculateFileHash(filePath); const currentHash = await this.calculateFileHash(filePath);
if (currentHash && currentHash !== file.hash) { if (currentHash && currentHash !== file.hash) {
result.modified.push(file.path); result.modified.push(file.path);
} }
} else {
result.missing.push(file.path);
} }
} }
@ -228,7 +199,7 @@ class FileManager {
} }
async backupFile(filePath) { async backupFile(filePath) {
const backupPath = filePath + ".bak"; const backupPath = filePath + '.bak';
let counter = 1; let counter = 1;
let finalBackupPath = backupPath; let finalBackupPath = backupPath;
@ -256,7 +227,7 @@ class FileManager {
} }
async readFile(filePath) { async readFile(filePath) {
return fs.readFile(filePath, "utf8"); return fs.readFile(filePath, 'utf8');
} }
async writeFile(filePath, content) { async writeFile(filePath, content) {
@ -269,14 +240,10 @@ class FileManager {
} }
async createExpansionPackManifest(installDir, packId, config, files) { async createExpansionPackManifest(installDir, packId, config, files) {
const manifestPath = path.join( const manifestPath = path.join(installDir, `.${packId}`, this.manifestFile);
installDir,
`.${packId}`,
this.manifestFile
);
const manifest = { const manifest = {
version: config.expansionPackVersion || require("../../../package.json").version, version: config.expansionPackVersion || require('../../../package.json').version,
installed_at: new Date().toISOString(), installed_at: new Date().toISOString(),
install_type: config.installType, install_type: config.installType,
expansion_pack_id: config.expansionPackId, expansion_pack_id: config.expansionPackId,
@ -336,26 +303,27 @@ class FileManager {
// Check file size to determine if we should stream // Check file size to determine if we should stream
const stats = await fs.stat(source); const stats = await fs.stat(source);
if (stats.size > 5 * 1024 * 1024) { // 5MB threshold if (stats.size > 5 * 1024 * 1024) {
// 5MB threshold
// Use streaming for large files // Use streaming for large files
const { Transform } = require('stream'); const { Transform } = require('node:stream');
const replaceStream = new Transform({ const replaceStream = new Transform({
transform(chunk, encoding, callback) { transform(chunk, encoding, callback) {
const modified = chunk.toString().replace(/\{root\}/g, rootValue); const modified = chunk.toString().replaceAll('{root}', rootValue);
callback(null, modified); callback(null, modified);
} },
}); });
await this.ensureDirectory(path.dirname(destination)); await this.ensureDirectory(path.dirname(destination));
await pipeline( await pipeline(
createReadStream(source, { encoding: 'utf8' }), createReadStream(source, { encoding: 'utf8' }),
replaceStream, replaceStream,
createWriteStream(destination, { encoding: 'utf8' }) createWriteStream(destination, { encoding: 'utf8' }),
); );
} else { } else {
// Regular approach for smaller files // Regular approach for smaller files
const content = await fsPromises.readFile(source, 'utf8'); const content = await fsPromises.readFile(source, 'utf8');
const updatedContent = content.replace(/\{root\}/g, rootValue); const updatedContent = content.replaceAll('{root}', rootValue);
await this.ensureDirectory(path.dirname(destination)); await this.ensureDirectory(path.dirname(destination));
await fsPromises.writeFile(destination, updatedContent, 'utf8'); await fsPromises.writeFile(destination, updatedContent, 'utf8');
} }
@ -367,32 +335,37 @@ class FileManager {
} }
} }
async copyDirectoryWithRootReplacement(source, destination, rootValue, fileExtensions = ['.md', '.yaml', '.yml']) { async copyDirectoryWithRootReplacement(
source,
destination,
rootValue,
fileExtensions = ['.md', '.yaml', '.yml'],
) {
try { try {
await this.ensureDirectory(destination); await this.ensureDirectory(destination);
// Get all files in source directory // Get all files in source directory
const files = await resourceLocator.findFiles('**/*', { const files = await resourceLocator.findFiles('**/*', {
cwd: source, cwd: source,
nodir: true nodir: true,
}); });
let replacedCount = 0; let replacedCount = 0;
for (const file of files) { for (const file of files) {
const sourcePath = path.join(source, file); const sourcePath = path.join(source, file);
const destPath = path.join(destination, file); const destinationPath = path.join(destination, file);
// Check if this file type should have {root} replacement // Check if this file type should have {root} replacement
const shouldReplace = fileExtensions.some(ext => file.endsWith(ext)); const shouldReplace = fileExtensions.some((extension) => file.endsWith(extension));
if (shouldReplace) { if (shouldReplace) {
if (await this.copyFileWithRootReplacement(sourcePath, destPath, rootValue)) { if (await this.copyFileWithRootReplacement(sourcePath, destinationPath, rootValue)) {
replacedCount++; replacedCount++;
} }
} else { } else {
// Regular copy for files that don't need replacement // Regular copy for files that don't need replacement
await this.copyFile(sourcePath, destPath); await this.copyFile(sourcePath, destinationPath);
} }
} }
@ -402,10 +375,15 @@ class FileManager {
return true; return true;
} catch (error) { } catch (error) {
console.error(chalk.red(`Failed to copy directory ${source} with root replacement:`), error.message); console.error(
chalk.red(`Failed to copy directory ${source} with root replacement:`),
error.message,
);
return false; return false;
} }
} }
manifestDir = '.bmad-core';
manifestFile = 'install-manifest.yaml';
} }
module.exports = new FileManager(); module.exports = new FileManager();

View File

@ -3,13 +3,13 @@
* Reduces duplication and provides shared methods * Reduces duplication and provides shared methods
*/ */
const path = require("path"); const path = require('node:path');
const fs = require("fs-extra"); const fs = require('fs-extra');
const yaml = require("js-yaml"); const yaml = require('js-yaml');
const chalk = require("chalk").default || require("chalk"); const chalk = require('chalk').default || require('chalk');
const fileManager = require("./file-manager"); const fileManager = require('./file-manager');
const resourceLocator = require("./resource-locator"); const resourceLocator = require('./resource-locator');
const { extractYamlFromAgent } = require("../../lib/yaml-utils"); const { extractYamlFromAgent } = require('../../lib/yaml-utils');
class BaseIdeSetup { class BaseIdeSetup {
constructor() { constructor() {
@ -30,16 +30,16 @@ class BaseIdeSetup {
// Get core agents // Get core agents
const coreAgents = await this.getCoreAgentIds(installDir); const coreAgents = await this.getCoreAgentIds(installDir);
coreAgents.forEach(id => allAgents.add(id)); for (const id of coreAgents) allAgents.add(id);
// Get expansion pack agents // Get expansion pack agents
const expansionPacks = await this.getInstalledExpansionPacks(installDir); const expansionPacks = await this.getInstalledExpansionPacks(installDir);
for (const pack of expansionPacks) { for (const pack of expansionPacks) {
const packAgents = await this.getExpansionPackAgents(pack.path); const packAgents = await this.getExpansionPackAgents(pack.path);
packAgents.forEach(id => allAgents.add(id)); for (const id of packAgents) allAgents.add(id);
} }
const result = Array.from(allAgents); const result = [...allAgents];
this._agentCache.set(cacheKey, result); this._agentCache.set(cacheKey, result);
return result; return result;
} }
@ -50,14 +50,14 @@ class BaseIdeSetup {
async getCoreAgentIds(installDir) { async getCoreAgentIds(installDir) {
const coreAgents = []; const coreAgents = [];
const corePaths = [ const corePaths = [
path.join(installDir, ".bmad-core", "agents"), path.join(installDir, '.bmad-core', 'agents'),
path.join(installDir, "bmad-core", "agents") path.join(installDir, 'bmad-core', 'agents'),
]; ];
for (const agentsDir of corePaths) { for (const agentsDir of corePaths) {
if (await fileManager.pathExists(agentsDir)) { if (await fileManager.pathExists(agentsDir)) {
const files = await resourceLocator.findFiles("*.md", { cwd: agentsDir }); const files = await resourceLocator.findFiles('*.md', { cwd: agentsDir });
coreAgents.push(...files.map(file => path.basename(file, ".md"))); coreAgents.push(...files.map((file) => path.basename(file, '.md')));
break; // Use first found break; // Use first found
} }
} }
@ -80,9 +80,9 @@ class BaseIdeSetup {
if (!agentPath) { if (!agentPath) {
// Check installation-specific paths // Check installation-specific paths
const possiblePaths = [ const possiblePaths = [
path.join(installDir, ".bmad-core", "agents", `${agentId}.md`), path.join(installDir, '.bmad-core', 'agents', `${agentId}.md`),
path.join(installDir, "bmad-core", "agents", `${agentId}.md`), path.join(installDir, 'bmad-core', 'agents', `${agentId}.md`),
path.join(installDir, "common", "agents", `${agentId}.md`) path.join(installDir, 'common', 'agents', `${agentId}.md`),
]; ];
for (const testPath of possiblePaths) { for (const testPath of possiblePaths) {
@ -113,7 +113,7 @@ class BaseIdeSetup {
const metadata = yaml.load(yamlContent); const metadata = yaml.load(yamlContent);
return metadata.agent_name || agentId; return metadata.agent_name || agentId;
} }
} catch (error) { } catch {
// Fallback to agent ID // Fallback to agent ID
} }
return agentId; return agentId;
@ -131,29 +131,29 @@ class BaseIdeSetup {
const expansionPacks = []; const expansionPacks = [];
// Check for dot-prefixed expansion packs // Check for dot-prefixed expansion packs
const dotExpansions = await resourceLocator.findFiles(".bmad-*", { cwd: installDir }); const dotExpansions = await resourceLocator.findFiles('.bmad-*', { cwd: installDir });
for (const dotExpansion of dotExpansions) { for (const dotExpansion of dotExpansions) {
if (dotExpansion !== ".bmad-core") { if (dotExpansion !== '.bmad-core') {
const packPath = path.join(installDir, dotExpansion); const packPath = path.join(installDir, dotExpansion);
const packName = dotExpansion.substring(1); // remove the dot const packName = dotExpansion.slice(1); // remove the dot
expansionPacks.push({ expansionPacks.push({
name: packName, name: packName,
path: packPath path: packPath,
}); });
} }
} }
// Check other dot folders that have config.yaml // Check other dot folders that have config.yaml
const allDotFolders = await resourceLocator.findFiles(".*", { cwd: installDir }); const allDotFolders = await resourceLocator.findFiles('.*', { cwd: installDir });
for (const folder of allDotFolders) { for (const folder of allDotFolders) {
if (!folder.startsWith(".bmad-") && folder !== ".bmad-core") { if (!folder.startsWith('.bmad-') && folder !== '.bmad-core') {
const packPath = path.join(installDir, folder); const packPath = path.join(installDir, folder);
const configPath = path.join(packPath, "config.yaml"); const configPath = path.join(packPath, 'config.yaml');
if (await fileManager.pathExists(configPath)) { if (await fileManager.pathExists(configPath)) {
expansionPacks.push({ expansionPacks.push({
name: folder.substring(1), // remove the dot name: folder.slice(1), // remove the dot
path: packPath path: packPath,
}); });
} }
} }
@ -167,13 +167,13 @@ class BaseIdeSetup {
* Get expansion pack agents * Get expansion pack agents
*/ */
async getExpansionPackAgents(packPath) { async getExpansionPackAgents(packPath) {
const agentsDir = path.join(packPath, "agents"); const agentsDir = path.join(packPath, 'agents');
if (!(await fileManager.pathExists(agentsDir))) { if (!(await fileManager.pathExists(agentsDir))) {
return []; return [];
} }
const agentFiles = await resourceLocator.findFiles("*.md", { cwd: agentsDir }); const agentFiles = await resourceLocator.findFiles('*.md', { cwd: agentsDir });
return agentFiles.map(file => path.basename(file, ".md")); return agentFiles.map((file) => path.basename(file, '.md'));
} }
/** /**
@ -184,26 +184,27 @@ class BaseIdeSetup {
const agentTitle = await this.getAgentTitle(agentId, installDir); const agentTitle = await this.getAgentTitle(agentId, installDir);
const yamlContent = extractYamlFromAgent(agentContent); const yamlContent = extractYamlFromAgent(agentContent);
let content = ""; let content = '';
if (format === 'mdc') { if (format === 'mdc') {
// MDC format for Cursor // MDC format for Cursor
content = "---\n"; content = '---\n';
content += "description: \n"; content += 'description: \n';
content += "globs: []\n"; content += 'globs: []\n';
content += "alwaysApply: false\n"; content += 'alwaysApply: false\n';
content += "---\n\n"; content += '---\n\n';
content += `# ${agentId.toUpperCase()} Agent Rule\n\n`; content += `# ${agentId.toUpperCase()} Agent Rule\n\n`;
content += `This rule is triggered when the user types \`@${agentId}\` and activates the ${agentTitle} agent persona.\n\n`; content += `This rule is triggered when the user types \`@${agentId}\` and activates the ${agentTitle} agent persona.\n\n`;
content += "## Agent Activation\n\n"; content += '## Agent Activation\n\n';
content += "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n"; content +=
content += "```yaml\n"; 'CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n';
content += yamlContent || agentContent.replace(/^#.*$/m, "").trim(); content += '```yaml\n';
content += "\n```\n\n"; content += yamlContent || agentContent.replace(/^#.*$/m, '').trim();
content += "## File Reference\n\n"; content += '\n```\n\n';
const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/'); content += '## File Reference\n\n';
const relativePath = path.relative(installDir, agentPath).replaceAll('\\', '/');
content += `The complete agent definition is available in [${relativePath}](mdc:${relativePath}).\n\n`; content += `The complete agent definition is available in [${relativePath}](mdc:${relativePath}).\n\n`;
content += "## Usage\n\n"; content += '## Usage\n\n';
content += `When the user types \`@${agentId}\`, activate this ${agentTitle} persona and follow all instructions defined in the YAML configuration above.\n`; content += `When the user types \`@${agentId}\`, activate this ${agentTitle} persona and follow all instructions defined in the YAML configuration above.\n`;
} else if (format === 'claude') { } else if (format === 'claude') {
// Claude Code format // Claude Code format

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
* Helps identify memory leaks and optimize resource usage * Helps identify memory leaks and optimize resource usage
*/ */
const v8 = require('v8'); const v8 = require('node:v8');
class MemoryProfiler { class MemoryProfiler {
constructor() { constructor() {
@ -28,18 +28,18 @@ class MemoryProfiler {
heapTotal: this.formatBytes(memUsage.heapTotal), heapTotal: this.formatBytes(memUsage.heapTotal),
heapUsed: this.formatBytes(memUsage.heapUsed), heapUsed: this.formatBytes(memUsage.heapUsed),
external: this.formatBytes(memUsage.external), external: this.formatBytes(memUsage.external),
arrayBuffers: this.formatBytes(memUsage.arrayBuffers || 0) arrayBuffers: this.formatBytes(memUsage.arrayBuffers || 0),
}, },
heap: { heap: {
totalHeapSize: this.formatBytes(heapStats.total_heap_size), totalHeapSize: this.formatBytes(heapStats.total_heap_size),
usedHeapSize: this.formatBytes(heapStats.used_heap_size), usedHeapSize: this.formatBytes(heapStats.used_heap_size),
heapSizeLimit: this.formatBytes(heapStats.heap_size_limit), heapSizeLimit: this.formatBytes(heapStats.heap_size_limit),
mallocedMemory: this.formatBytes(heapStats.malloced_memory), mallocedMemory: this.formatBytes(heapStats.malloced_memory),
externalMemory: this.formatBytes(heapStats.external_memory) externalMemory: this.formatBytes(heapStats.external_memory),
}, },
raw: { raw: {
heapUsed: memUsage.heapUsed heapUsed: memUsage.heapUsed,
} },
}; };
// Track peak memory // Track peak memory
@ -55,8 +55,8 @@ class MemoryProfiler {
* Force garbage collection (requires --expose-gc flag) * Force garbage collection (requires --expose-gc flag)
*/ */
forceGC() { forceGC() {
if (global.gc) { if (globalThis.gc) {
global.gc(); globalThis.gc();
return true; return true;
} }
return false; return false;
@ -72,11 +72,11 @@ class MemoryProfiler {
currentUsage: { currentUsage: {
rss: this.formatBytes(currentMemory.rss), rss: this.formatBytes(currentMemory.rss),
heapTotal: this.formatBytes(currentMemory.heapTotal), heapTotal: this.formatBytes(currentMemory.heapTotal),
heapUsed: this.formatBytes(currentMemory.heapUsed) heapUsed: this.formatBytes(currentMemory.heapUsed),
}, },
peakMemory: this.formatBytes(this.peakMemory), peakMemory: this.formatBytes(this.peakMemory),
totalCheckpoints: this.checkpoints.length, totalCheckpoints: this.checkpoints.length,
runTime: `${((Date.now() - this.startTime) / 1000).toFixed(2)}s` runTime: `${((Date.now() - this.startTime) / 1000).toFixed(2)}s`,
}; };
} }
@ -91,7 +91,7 @@ class MemoryProfiler {
summary, summary,
memoryGrowth, memoryGrowth,
checkpoints: this.checkpoints, checkpoints: this.checkpoints,
recommendations: this.getRecommendations(memoryGrowth) recommendations: this.getRecommendations(memoryGrowth),
}; };
} }
@ -102,18 +102,18 @@ class MemoryProfiler {
if (this.checkpoints.length < 2) return []; if (this.checkpoints.length < 2) return [];
const growth = []; const growth = [];
for (let i = 1; i < this.checkpoints.length; i++) { for (let index = 1; index < this.checkpoints.length; index++) {
const prev = this.checkpoints[i - 1]; const previous = this.checkpoints[index - 1];
const curr = this.checkpoints[i]; const current = this.checkpoints[index];
const heapDiff = curr.raw.heapUsed - prev.raw.heapUsed; const heapDiff = current.raw.heapUsed - previous.raw.heapUsed;
growth.push({ growth.push({
from: prev.label, from: previous.label,
to: curr.label, to: current.label,
heapGrowth: this.formatBytes(Math.abs(heapDiff)), heapGrowth: this.formatBytes(Math.abs(heapDiff)),
isIncrease: heapDiff > 0, isIncrease: heapDiff > 0,
timeDiff: `${((curr.timestamp - prev.timestamp) / 1000).toFixed(2)}s` timeDiff: `${((current.timestamp - previous.timestamp) / 1000).toFixed(2)}s`,
}); });
} }
@ -127,7 +127,7 @@ class MemoryProfiler {
const recommendations = []; const recommendations = [];
// Check for large memory growth // Check for large memory growth
const largeGrowths = memoryGrowth.filter(g => { const largeGrowths = memoryGrowth.filter((g) => {
const bytes = this.parseBytes(g.heapGrowth); const bytes = this.parseBytes(g.heapGrowth);
return bytes > 50 * 1024 * 1024; // 50MB return bytes > 50 * 1024 * 1024; // 50MB
}); });
@ -136,16 +136,17 @@ class MemoryProfiler {
recommendations.push({ recommendations.push({
type: 'warning', type: 'warning',
message: `Large memory growth detected in ${largeGrowths.length} operations`, message: `Large memory growth detected in ${largeGrowths.length} operations`,
details: largeGrowths.map(g => `${g.from}${g.to}: ${g.heapGrowth}`) details: largeGrowths.map((g) => `${g.from}${g.to}: ${g.heapGrowth}`),
}); });
} }
// Check peak memory // Check peak memory
if (this.peakMemory > 500 * 1024 * 1024) { // 500MB if (this.peakMemory > 500 * 1024 * 1024) {
// 500MB
recommendations.push({ recommendations.push({
type: 'warning', type: 'warning',
message: `High peak memory usage: ${this.formatBytes(this.peakMemory)}`, message: `High peak memory usage: ${this.formatBytes(this.peakMemory)}`,
suggestion: 'Consider processing files in smaller batches' suggestion: 'Consider processing files in smaller batches',
}); });
} }
@ -155,7 +156,7 @@ class MemoryProfiler {
recommendations.push({ recommendations.push({
type: 'error', type: 'error',
message: 'Potential memory leak detected', message: 'Potential memory leak detected',
details: 'Memory usage continuously increases without significant decreases' details: 'Memory usage continuously increases without significant decreases',
}); });
} }
@ -169,8 +170,8 @@ class MemoryProfiler {
if (this.checkpoints.length < 5) return false; if (this.checkpoints.length < 5) return false;
let increasingCount = 0; let increasingCount = 0;
for (let i = 1; i < this.checkpoints.length; i++) { for (let index = 1; index < this.checkpoints.length; index++) {
if (this.checkpoints[i].raw.heapUsed > this.checkpoints[i - 1].raw.heapUsed) { if (this.checkpoints[index].raw.heapUsed > this.checkpoints[index - 1].raw.heapUsed) {
increasingCount++; increasingCount++;
} }
} }
@ -187,26 +188,26 @@ class MemoryProfiler {
const k = 1024; const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB']; const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const index = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return Number.parseFloat((bytes / Math.pow(k, index)).toFixed(2)) + ' ' + sizes[index];
} }
/** /**
* Parse human-readable bytes back to number * Parse human-readable bytes back to number
*/ */
parseBytes(str) { parseBytes(string_) {
const match = str.match(/^([\d.]+)\s*([KMGT]?B?)$/i); const match = string_.match(/^([\d.]+)\s*([KMGT]?B?)$/i);
if (!match) return 0; if (!match) return 0;
const value = parseFloat(match[1]); const value = Number.parseFloat(match[1]);
const unit = match[2].toUpperCase(); const unit = match[2].toUpperCase();
const multipliers = { const multipliers = {
'B': 1, B: 1,
'KB': 1024, KB: 1024,
'MB': 1024 * 1024, MB: 1024 * 1024,
'GB': 1024 * 1024 * 1024 GB: 1024 * 1024 * 1024,
}; };
return value * (multipliers[unit] || 1); return value * (multipliers[unit] || 1);

View File

@ -17,13 +17,13 @@ class ModuleManager {
const modules = await Promise.all([ const modules = await Promise.all([
this.getModule('chalk'), this.getModule('chalk'),
this.getModule('ora'), this.getModule('ora'),
this.getModule('inquirer') this.getModule('inquirer'),
]); ]);
return { return {
chalk: modules[0], chalk: modules[0],
ora: modules[1], ora: modules[1],
inquirer: modules[2] inquirer: modules[2],
}; };
} }
@ -64,20 +64,26 @@ class ModuleManager {
*/ */
async _loadModule(moduleName) { async _loadModule(moduleName) {
switch (moduleName) { switch (moduleName) {
case 'chalk': case 'chalk': {
return (await import('chalk')).default; return (await import('chalk')).default;
case 'ora': }
case 'ora': {
return (await import('ora')).default; return (await import('ora')).default;
case 'inquirer': }
case 'inquirer': {
return (await import('inquirer')).default; return (await import('inquirer')).default;
case 'glob': }
case 'glob': {
return (await import('glob')).glob; return (await import('glob')).glob;
case 'globSync': }
case 'globSync': {
return (await import('glob')).globSync; return (await import('glob')).globSync;
default: }
default: {
throw new Error(`Unknown module: ${moduleName}`); throw new Error(`Unknown module: ${moduleName}`);
} }
} }
}
/** /**
* Clear the module cache to free memory * Clear the module cache to free memory
@ -93,13 +99,11 @@ class ModuleManager {
* @returns {Promise<Object>} Object with module names as keys * @returns {Promise<Object>} Object with module names as keys
*/ */
async getModules(moduleNames) { async getModules(moduleNames) {
const modules = await Promise.all( const modules = await Promise.all(moduleNames.map((name) => this.getModule(name)));
moduleNames.map(name => this.getModule(name))
);
return moduleNames.reduce((acc, name, index) => { return moduleNames.reduce((accumulator, name, index) => {
acc[name] = modules[index]; accumulator[name] = modules[index];
return acc; return accumulator;
}, {}); }, {});
} }
} }

View File

@ -107,14 +107,11 @@ class ResourceLocator {
// Get agents from bmad-core // Get agents from bmad-core
const coreAgents = await this.findFiles('agents/*.md', { const coreAgents = await this.findFiles('agents/*.md', {
cwd: this.getBmadCorePath() cwd: this.getBmadCorePath(),
}); });
for (const agentFile of coreAgents) { for (const agentFile of coreAgents) {
const content = await fs.readFile( const content = await fs.readFile(path.join(this.getBmadCorePath(), agentFile), 'utf8');
path.join(this.getBmadCorePath(), agentFile),
'utf8'
);
const yamlContent = extractYamlFromAgent(content); const yamlContent = extractYamlFromAgent(content);
if (yamlContent) { if (yamlContent) {
try { try {
@ -123,9 +120,9 @@ class ResourceLocator {
id: path.basename(agentFile, '.md'), id: path.basename(agentFile, '.md'),
name: metadata.agent_name || path.basename(agentFile, '.md'), name: metadata.agent_name || path.basename(agentFile, '.md'),
description: metadata.description || 'No description available', description: metadata.description || 'No description available',
source: 'core' source: 'core',
}); });
} catch (e) { } catch {
// Skip invalid agents // Skip invalid agents
} }
} }
@ -167,11 +164,12 @@ class ResourceLocator {
name: config.name || entry.name, name: config.name || entry.name,
version: config.version || '1.0.0', version: config.version || '1.0.0',
description: config.description || 'No description available', description: config.description || 'No description available',
shortTitle: config['short-title'] || config.description || 'No description available', shortTitle:
config['short-title'] || config.description || 'No description available',
author: config.author || 'Unknown', author: config.author || 'Unknown',
path: path.join(expansionPacksPath, entry.name) path: path.join(expansionPacksPath, entry.name),
}); });
} catch (e) { } catch {
// Skip invalid packs // Skip invalid packs
} }
} }
@ -207,7 +205,7 @@ class ResourceLocator {
const config = yaml.load(content); const config = yaml.load(content);
this._pathCache.set(cacheKey, config); this._pathCache.set(cacheKey, config);
return config; return config;
} catch (e) { } catch {
return null; return null;
} }
} }
@ -261,7 +259,7 @@ class ResourceLocator {
const result = { all: allDeps, byType }; const result = { all: allDeps, byType };
this._pathCache.set(cacheKey, result); this._pathCache.set(cacheKey, result);
return result; return result;
} catch (e) { } catch {
return { all: [], byType: {} }; return { all: [], byType: {} };
} }
} }
@ -295,7 +293,7 @@ class ResourceLocator {
const config = yaml.load(content); const config = yaml.load(content);
this._pathCache.set(cacheKey, config); this._pathCache.set(cacheKey, config);
return config; return config;
} catch (e) { } catch {
return null; return null;
} }
} }

View File

@ -2,14 +2,6 @@
"name": "bmad-method", "name": "bmad-method",
"version": "5.0.0", "version": "5.0.0",
"description": "BMad Method installer - AI-powered Agile development framework", "description": "BMad Method installer - AI-powered Agile development framework",
"main": "lib/installer.js",
"bin": {
"bmad": "./bin/bmad.js",
"bmad-method": "./bin/bmad.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [ "keywords": [
"bmad", "bmad",
"agile", "agile",
@ -19,8 +11,24 @@
"installer", "installer",
"agents" "agents"
], ],
"author": "BMad Team", "homepage": "https://github.com/bmad-team/bmad-method#readme",
"bugs": {
"url": "https://github.com/bmad-team/bmad-method/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/bmad-team/bmad-method.git"
},
"license": "MIT", "license": "MIT",
"author": "BMad Team",
"main": "lib/installer.js",
"bin": {
"bmad": "./bin/bmad.js",
"bmad-method": "./bin/bmad.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
"commander": "^14.0.0", "commander": "^14.0.0",
@ -32,13 +40,5 @@
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, }
"repository": {
"type": "git",
"url": "https://github.com/bmad-team/bmad-method.git"
},
"bugs": {
"url": "https://github.com/bmad-team/bmad-method/issues"
},
"homepage": "https://github.com/bmad-team/bmad-method#readme"
} }

View File

@ -1,5 +1,5 @@
const fs = require('fs').promises; const fs = require('node:fs').promises;
const path = require('path'); const path = require('node:path');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const { extractYamlFromAgent } = require('./yaml-utils'); const { extractYamlFromAgent } = require('./yaml-utils');
@ -28,9 +28,9 @@ class DependencyResolver {
id: agentId, id: agentId,
path: agentPath, path: agentPath,
content: agentContent, content: agentContent,
config: agentConfig config: agentConfig,
}, },
resources: [] resources: [],
}; };
// Personas are now embedded in agent configs, no need to resolve separately // Personas are now embedded in agent configs, no need to resolve separately
@ -58,18 +58,18 @@ class DependencyResolver {
id: teamId, id: teamId,
path: teamPath, path: teamPath,
content: teamContent, content: teamContent,
config: teamConfig config: teamConfig,
}, },
agents: [], agents: [],
resources: new Map() // Use Map to deduplicate resources resources: new Map(), // Use Map to deduplicate resources
}; };
// Always add bmad-orchestrator agent first if it's a team // Always add bmad-orchestrator agent first if it's a team
const bmadAgent = await this.resolveAgentDependencies('bmad-orchestrator'); const bmadAgent = await this.resolveAgentDependencies('bmad-orchestrator');
dependencies.agents.push(bmadAgent.agent); dependencies.agents.push(bmadAgent.agent);
bmadAgent.resources.forEach(res => { for (const res of bmadAgent.resources) {
dependencies.resources.set(res.path, res); dependencies.resources.set(res.path, res);
}); }
// Resolve all agents in the team // Resolve all agents in the team
let agentsToResolve = teamConfig.agents || []; let agentsToResolve = teamConfig.agents || [];
@ -78,7 +78,7 @@ class DependencyResolver {
if (agentsToResolve.includes('*')) { if (agentsToResolve.includes('*')) {
const allAgents = await this.listAgents(); const allAgents = await this.listAgents();
// Remove wildcard and add all agents except those already in the list and bmad-master // Remove wildcard and add all agents except those already in the list and bmad-master
agentsToResolve = agentsToResolve.filter(a => a !== '*'); agentsToResolve = agentsToResolve.filter((a) => a !== '*');
for (const agent of allAgents) { for (const agent of allAgents) {
if (!agentsToResolve.includes(agent) && agent !== 'bmad-master') { if (!agentsToResolve.includes(agent) && agent !== 'bmad-master') {
agentsToResolve.push(agent); agentsToResolve.push(agent);
@ -92,9 +92,9 @@ class DependencyResolver {
dependencies.agents.push(agentDeps.agent); dependencies.agents.push(agentDeps.agent);
// Add resources with deduplication // Add resources with deduplication
agentDeps.resources.forEach(res => { for (const res of agentDeps.resources) {
dependencies.resources.set(res.path, res); dependencies.resources.set(res.path, res);
}); }
} }
// Resolve workflows // Resolve workflows
@ -104,7 +104,7 @@ class DependencyResolver {
} }
// Convert Map back to array // Convert Map back to array
dependencies.resources = Array.from(dependencies.resources.values()); dependencies.resources = [...dependencies.resources.values()];
return dependencies; return dependencies;
} }
@ -123,12 +123,12 @@ class DependencyResolver {
try { try {
filePath = path.join(this.bmadCore, type, id); filePath = path.join(this.bmadCore, type, id);
content = await fs.readFile(filePath, 'utf8'); content = await fs.readFile(filePath, 'utf8');
} catch (e) { } catch {
// If not found in bmad-core, try common folder // If not found in bmad-core, try common folder
try { try {
filePath = path.join(this.common, type, id); filePath = path.join(this.common, type, id);
content = await fs.readFile(filePath, 'utf8'); content = await fs.readFile(filePath, 'utf8');
} catch (e2) { } catch {
// File not found in either location // File not found in either location
} }
} }
@ -142,7 +142,7 @@ class DependencyResolver {
type, type,
id, id,
path: filePath, path: filePath,
content content,
}; };
this.cache.set(cacheKey, resource); this.cache.set(cacheKey, resource);
@ -156,10 +156,8 @@ class DependencyResolver {
async listAgents() { async listAgents() {
try { try {
const files = await fs.readdir(path.join(this.bmadCore, 'agents')); const files = await fs.readdir(path.join(this.bmadCore, 'agents'));
return files return files.filter((f) => f.endsWith('.md')).map((f) => f.replace('.md', ''));
.filter(f => f.endsWith('.md')) } catch {
.map(f => f.replace('.md', ''));
} catch (error) {
return []; return [];
} }
} }
@ -167,10 +165,8 @@ class DependencyResolver {
async listTeams() { async listTeams() {
try { try {
const files = await fs.readdir(path.join(this.bmadCore, 'agent-teams')); const files = await fs.readdir(path.join(this.bmadCore, 'agent-teams'));
return files return files.filter((f) => f.endsWith('.yaml')).map((f) => f.replace('.yaml', ''));
.filter(f => f.endsWith('.yaml')) } catch {
.map(f => f.replace('.yaml', ''));
} catch (error) {
return []; return [];
} }
} }

View File

@ -10,7 +10,7 @@
*/ */
function extractYamlFromAgent(agentContent, cleanCommands = false) { function extractYamlFromAgent(agentContent, cleanCommands = false) {
// Remove carriage returns and match YAML block // Remove carriage returns and match YAML block
const yamlMatch = agentContent.replace(/\r/g, "").match(/```ya?ml\n([\s\S]*?)\n```/); const yamlMatch = agentContent.replaceAll('\r', '').match(/```ya?ml\n([\s\S]*?)\n```/);
if (!yamlMatch) return null; if (!yamlMatch) return null;
let yamlContent = yamlMatch[1].trim(); let yamlContent = yamlMatch[1].trim();
@ -18,12 +18,12 @@ function extractYamlFromAgent(agentContent, cleanCommands = false) {
// Clean up command descriptions if requested // Clean up command descriptions if requested
// Converts "- command - description" to just "- command" // Converts "- command - description" to just "- command"
if (cleanCommands) { if (cleanCommands) {
yamlContent = yamlContent.replace(/^(\s*-)(\s*"[^"]+")(\s*-\s*.*)$/gm, '$1$2'); yamlContent = yamlContent.replaceAll(/^(\s*-)(\s*"[^"]+")(\s*-\s*.*)$/gm, '$1$2');
} }
return yamlContent; return yamlContent;
} }
module.exports = { module.exports = {
extractYamlFromAgent extractYamlFromAgent,
}; };

View File

@ -2,8 +2,8 @@
* Semantic-release plugin to sync installer package.json version * Semantic-release plugin to sync installer package.json version
*/ */
const fs = require('fs'); const fs = require('node:fs');
const path = require('path'); const path = require('node:path');
// This function runs during the "prepare" step of semantic-release // This function runs during the "prepare" step of semantic-release
function prepare(_, { nextRelease, logger }) { function prepare(_, { nextRelease, logger }) {
@ -14,13 +14,13 @@ function prepare(_, { nextRelease, logger }) {
if (!fs.existsSync(file)) return logger.log('Installer package.json not found, skipping'); if (!fs.existsSync(file)) return logger.log('Installer package.json not found, skipping');
// Read and parse the package.json file // Read and parse the package.json file
const pkg = JSON.parse(fs.readFileSync(file, 'utf8')); const package_ = JSON.parse(fs.readFileSync(file, 'utf8'));
// Update the version field with the next release version // Update the version field with the next release version
pkg.version = nextRelease.version; package_.version = nextRelease.version;
// Write the updated JSON back to the file // Write the updated JSON back to the file
fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n'); fs.writeFileSync(file, JSON.stringify(package_, null, 2) + '\n');
// Log success message // Log success message
logger.log(`Synced installer package.json to version ${nextRelease.version}`); logger.log(`Synced installer package.json to version ${nextRelease.version}`);

View File

@ -1,8 +1,8 @@
// ASCII banner art definitions extracted from banners.js to separate art from logic // ASCII banner art definitions extracted from banners.js to separate art from logic
const BMAD_TITLE = "BMAD-METHOD"; const BMAD_TITLE = 'BMAD-METHOD';
const FLATTENER_TITLE = "FLATTENER"; const FLATTENER_TITLE = 'FLATTENER';
const INSTALLER_TITLE = "INSTALLER"; const INSTALLER_TITLE = 'INSTALLER';
// Large ASCII blocks (block-style fonts) // Large ASCII blocks (block-style fonts)
const BMAD_LARGE = ` const BMAD_LARGE = `

View File

@ -1,12 +1,10 @@
#!/usr/bin/env node
/** /**
* Sync installer package.json version with main package.json * Sync installer package.json version with main package.json
* Used by semantic-release to keep versions in sync * Used by semantic-release to keep versions in sync
*/ */
const fs = require('fs'); const fs = require('node:fs');
const path = require('path'); const path = require('node:path');
function syncInstallerVersion() { function syncInstallerVersion() {
// Read main package.json // Read main package.json

View File

@ -1,18 +1,16 @@
#!/usr/bin/env node const fs = require('node:fs');
const path = require('node:path');
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const args = process.argv.slice(2); const arguments_ = process.argv.slice(2);
if (args.length < 2) { if (arguments_.length < 2) {
console.log('Usage: node update-expansion-version.js <expansion-pack-id> <new-version>'); console.log('Usage: node update-expansion-version.js <expansion-pack-id> <new-version>');
console.log('Example: node update-expansion-version.js bmad-creator-tools 1.1.0'); console.log('Example: node update-expansion-version.js bmad-creator-tools 1.1.0');
process.exit(1); process.exit(1);
} }
const [packId, newVersion] = args; const [packId, newVersion] = arguments_;
// Validate version format // Validate version format
if (!/^\d+\.\d+\.\d+$/.test(newVersion)) { if (!/^\d+\.\d+\.\d+$/.test(newVersion)) {
@ -43,8 +41,9 @@ async function updateVersion() {
console.log(`\n✓ Successfully updated ${packId} to version ${newVersion}`); console.log(`\n✓ Successfully updated ${packId} to version ${newVersion}`);
console.log('\nNext steps:'); console.log('\nNext steps:');
console.log('1. Test the changes'); console.log('1. Test the changes');
console.log('2. Commit: git add -A && git commit -m "chore: bump ' + packId + ' to v' + newVersion + '"'); console.log(
'2. Commit: git add -A && git commit -m "chore: bump ' + packId + ' to v' + newVersion + '"',
);
} catch (error) { } catch (error) {
console.error('Error updating version:', error.message); console.error('Error updating version:', error.message);
process.exit(1); process.exit(1);

View File

@ -1,15 +1,15 @@
const fs = require("fs").promises; const fs = require('node:fs').promises;
const path = require("path"); const path = require('node:path');
const { glob } = require("glob"); const { glob } = require('glob');
// Dynamic imports for ES modules // Dynamic imports for ES modules
let chalk, ora, inquirer; let chalk, ora, inquirer;
// Initialize ES modules // Initialize ES modules
async function initializeModules() { async function initializeModules() {
chalk = (await import("chalk")).default; chalk = (await import('chalk')).default;
ora = (await import("ora")).default; ora = (await import('ora')).default;
inquirer = (await import("inquirer")).default; inquirer = (await import('inquirer')).default;
} }
class V3ToV4Upgrader { class V3ToV4Upgrader {
@ -25,23 +25,15 @@ class V3ToV4Upgrader {
process.stdin.resume(); process.stdin.resume();
// 1. Welcome message // 1. Welcome message
console.log( console.log(chalk.bold('\nWelcome to BMad-Method V3 to V4 Upgrade Tool\n'));
chalk.bold("\nWelcome to BMad-Method V3 to V4 Upgrade Tool\n") console.log('This tool will help you upgrade your BMad-Method V3 project to V4.\n');
); console.log(chalk.cyan('What this tool does:'));
console.log( console.log('- Creates a backup of your V3 files (.bmad-v3-backup/)');
"This tool will help you upgrade your BMad-Method V3 project to V4.\n" console.log('- Installs the new V4 .bmad-core structure');
); console.log('- Preserves your PRD, Architecture, and Stories in the new format\n');
console.log(chalk.cyan("What this tool does:")); console.log(chalk.yellow('What this tool does NOT do:'));
console.log("- Creates a backup of your V3 files (.bmad-v3-backup/)"); console.log('- Modify your document content (use doc-migration-task after upgrade)');
console.log("- Installs the new V4 .bmad-core structure"); console.log('- Touch any files outside bmad-agent/ and docs/\n');
console.log(
"- Preserves your PRD, Architecture, and Stories in the new format\n"
);
console.log(chalk.yellow("What this tool does NOT do:"));
console.log(
"- Modify your document content (use doc-migration-task after upgrade)"
);
console.log("- Touch any files outside bmad-agent/ and docs/\n");
// 2. Get project path // 2. Get project path
const projectPath = await this.getProjectPath(options.projectPath); const projectPath = await this.getProjectPath(options.projectPath);
@ -49,15 +41,11 @@ class V3ToV4Upgrader {
// 3. Validate V3 structure // 3. Validate V3 structure
const validation = await this.validateV3Project(projectPath); const validation = await this.validateV3Project(projectPath);
if (!validation.isValid) { if (!validation.isValid) {
console.error( console.error(chalk.red("\nError: This doesn't appear to be a V3 project."));
chalk.red("\nError: This doesn't appear to be a V3 project.") console.error('Expected to find:');
); console.error('- bmad-agent/ directory');
console.error("Expected to find:"); console.error('- docs/ directory\n');
console.error("- bmad-agent/ directory"); console.error("Please check you're in the correct directory and try again.");
console.error("- docs/ directory\n");
console.error(
"Please check you're in the correct directory and try again."
);
return; return;
} }
@ -68,15 +56,15 @@ class V3ToV4Upgrader {
if (!options.dryRun) { if (!options.dryRun) {
const { confirm } = await inquirer.prompt([ const { confirm } = await inquirer.prompt([
{ {
type: "confirm", type: 'confirm',
name: "confirm", name: 'confirm',
message: "Continue with upgrade?", message: 'Continue with upgrade?',
default: true, default: true,
}, },
]); ]);
if (!confirm) { if (!confirm) {
console.log("Upgrade cancelled."); console.log('Upgrade cancelled.');
return; return;
} }
} }
@ -106,7 +94,7 @@ class V3ToV4Upgrader {
process.exit(0); process.exit(0);
} catch (error) { } catch (error) {
console.error(chalk.red("\nUpgrade error:"), error.message); console.error(chalk.red('\nUpgrade error:'), error.message);
process.exit(1); process.exit(1);
} }
} }
@ -118,9 +106,9 @@ class V3ToV4Upgrader {
const { projectPath } = await inquirer.prompt([ const { projectPath } = await inquirer.prompt([
{ {
type: "input", type: 'input',
name: "projectPath", name: 'projectPath',
message: "Please enter the path to your V3 project:", message: 'Please enter the path to your V3 project:',
default: process.cwd(), default: process.cwd(),
}, },
]); ]);
@ -129,45 +117,45 @@ class V3ToV4Upgrader {
} }
async validateV3Project(projectPath) { async validateV3Project(projectPath) {
const spinner = ora("Validating project structure...").start(); const spinner = ora('Validating project structure...').start();
try { try {
const bmadAgentPath = path.join(projectPath, "bmad-agent"); const bmadAgentPath = path.join(projectPath, 'bmad-agent');
const docsPath = path.join(projectPath, "docs"); const docsPath = path.join(projectPath, 'docs');
const hasBmadAgent = await this.pathExists(bmadAgentPath); const hasBmadAgent = await this.pathExists(bmadAgentPath);
const hasDocs = await this.pathExists(docsPath); const hasDocs = await this.pathExists(docsPath);
if (hasBmadAgent) { if (hasBmadAgent) {
spinner.text = "✓ Found bmad-agent/ directory"; spinner.text = '✓ Found bmad-agent/ directory';
console.log(chalk.green("\n✓ Found bmad-agent/ directory")); console.log(chalk.green('\n✓ Found bmad-agent/ directory'));
} }
if (hasDocs) { if (hasDocs) {
console.log(chalk.green("✓ Found docs/ directory")); console.log(chalk.green('✓ Found docs/ directory'));
} }
const isValid = hasBmadAgent && hasDocs; const isValid = hasBmadAgent && hasDocs;
if (isValid) { if (isValid) {
spinner.succeed("This appears to be a valid V3 project"); spinner.succeed('This appears to be a valid V3 project');
} else { } else {
spinner.fail("Invalid V3 project structure"); spinner.fail('Invalid V3 project structure');
} }
return { isValid, hasBmadAgent, hasDocs }; return { isValid, hasBmadAgent, hasDocs };
} catch (error) { } catch (error) {
spinner.fail("Validation failed"); spinner.fail('Validation failed');
throw error; throw error;
} }
} }
async analyzeProject(projectPath) { async analyzeProject(projectPath) {
const docsPath = path.join(projectPath, "docs"); const docsPath = path.join(projectPath, 'docs');
const bmadAgentPath = path.join(projectPath, "bmad-agent"); const bmadAgentPath = path.join(projectPath, 'bmad-agent');
// Find PRD // Find PRD
const prdCandidates = ["prd.md", "PRD.md", "product-requirements.md"]; const prdCandidates = ['prd.md', 'PRD.md', 'product-requirements.md'];
let prdFile = null; let prdFile = null;
for (const candidate of prdCandidates) { for (const candidate of prdCandidates) {
const candidatePath = path.join(docsPath, candidate); const candidatePath = path.join(docsPath, candidate);
@ -178,11 +166,7 @@ class V3ToV4Upgrader {
} }
// Find Architecture // Find Architecture
const archCandidates = [ const archCandidates = ['architecture.md', 'Architecture.md', 'technical-architecture.md'];
"architecture.md",
"Architecture.md",
"technical-architecture.md",
];
let archFile = null; let archFile = null;
for (const candidate of archCandidates) { for (const candidate of archCandidates) {
const candidatePath = path.join(docsPath, candidate); const candidatePath = path.join(docsPath, candidate);
@ -194,9 +178,9 @@ class V3ToV4Upgrader {
// Find Front-end Architecture (V3 specific) // Find Front-end Architecture (V3 specific)
const frontEndCandidates = [ const frontEndCandidates = [
"front-end-architecture.md", 'front-end-architecture.md',
"frontend-architecture.md", 'frontend-architecture.md',
"ui-architecture.md", 'ui-architecture.md',
]; ];
let frontEndArchFile = null; let frontEndArchFile = null;
for (const candidate of frontEndCandidates) { for (const candidate of frontEndCandidates) {
@ -209,10 +193,10 @@ class V3ToV4Upgrader {
// Find UX/UI spec // Find UX/UI spec
const uxSpecCandidates = [ const uxSpecCandidates = [
"ux-ui-spec.md", 'ux-ui-spec.md',
"ux-ui-specification.md", 'ux-ui-specification.md',
"ui-spec.md", 'ui-spec.md',
"ux-spec.md", 'ux-spec.md',
]; ];
let uxSpecFile = null; let uxSpecFile = null;
for (const candidate of uxSpecCandidates) { for (const candidate of uxSpecCandidates) {
@ -224,12 +208,7 @@ class V3ToV4Upgrader {
} }
// Find v0 prompt or UX prompt // Find v0 prompt or UX prompt
const uxPromptCandidates = [ const uxPromptCandidates = ['v0-prompt.md', 'ux-prompt.md', 'ui-prompt.md', 'design-prompt.md'];
"v0-prompt.md",
"ux-prompt.md",
"ui-prompt.md",
"design-prompt.md",
];
let uxPromptFile = null; let uxPromptFile = null;
for (const candidate of uxPromptCandidates) { for (const candidate of uxPromptCandidates) {
const candidatePath = path.join(docsPath, candidate); const candidatePath = path.join(docsPath, candidate);
@ -240,19 +219,19 @@ class V3ToV4Upgrader {
} }
// Find epic files // Find epic files
const epicFiles = await glob("epic*.md", { cwd: docsPath }); const epicFiles = await glob('epic*.md', { cwd: docsPath });
// Find story files // Find story files
const storiesPath = path.join(docsPath, "stories"); const storiesPath = path.join(docsPath, 'stories');
let storyFiles = []; let storyFiles = [];
if (await this.pathExists(storiesPath)) { if (await this.pathExists(storiesPath)) {
storyFiles = await glob("*.md", { cwd: storiesPath }); storyFiles = await glob('*.md', { cwd: storiesPath });
} }
// Count custom files in bmad-agent // Count custom files in bmad-agent
const bmadAgentFiles = await glob("**/*.md", { const bmadAgentFiles = await glob('**/*.md', {
cwd: bmadAgentPath, cwd: bmadAgentPath,
ignore: ["node_modules/**"], ignore: ['node_modules/**'],
}); });
return { return {
@ -268,279 +247,233 @@ class V3ToV4Upgrader {
} }
async showPreflightCheck(analysis, options) { async showPreflightCheck(analysis, options) {
console.log(chalk.bold("\nProject Analysis:")); console.log(chalk.bold('\nProject Analysis:'));
console.log( console.log(
`- PRD found: ${ `- PRD found: ${analysis.prdFile ? `docs/${analysis.prdFile}` : chalk.yellow('Not found')}`,
analysis.prdFile
? `docs/${analysis.prdFile}`
: chalk.yellow("Not found")
}`
); );
console.log( console.log(
`- Architecture found: ${ `- Architecture found: ${
analysis.archFile analysis.archFile ? `docs/${analysis.archFile}` : chalk.yellow('Not found')
? `docs/${analysis.archFile}` }`,
: chalk.yellow("Not found")
}`
); );
if (analysis.frontEndArchFile) { if (analysis.frontEndArchFile) {
console.log( console.log(`- Front-end Architecture found: docs/${analysis.frontEndArchFile}`);
`- Front-end Architecture found: docs/${analysis.frontEndArchFile}`
);
} }
console.log( console.log(
`- UX/UI Spec found: ${ `- UX/UI Spec found: ${
analysis.uxSpecFile analysis.uxSpecFile ? `docs/${analysis.uxSpecFile}` : chalk.yellow('Not found')
? `docs/${analysis.uxSpecFile}` }`,
: chalk.yellow("Not found")
}`
); );
console.log( console.log(
`- UX/Design Prompt found: ${ `- UX/Design Prompt found: ${
analysis.uxPromptFile analysis.uxPromptFile ? `docs/${analysis.uxPromptFile}` : chalk.yellow('Not found')
? `docs/${analysis.uxPromptFile}` }`,
: chalk.yellow("Not found")
}`
);
console.log(
`- Epic files found: ${analysis.epicFiles.length} files (epic*.md)`
);
console.log(
`- Stories found: ${analysis.storyFiles.length} files in docs/stories/`
); );
console.log(`- Epic files found: ${analysis.epicFiles.length} files (epic*.md)`);
console.log(`- Stories found: ${analysis.storyFiles.length} files in docs/stories/`);
console.log(`- Custom files in bmad-agent/: ${analysis.customFileCount}`); console.log(`- Custom files in bmad-agent/: ${analysis.customFileCount}`);
if (!options.dryRun) { if (!options.dryRun) {
console.log("\nThe following will be backed up to .bmad-v3-backup/:"); console.log('\nThe following will be backed up to .bmad-v3-backup/:');
console.log("- bmad-agent/ (entire directory)"); console.log('- bmad-agent/ (entire directory)');
console.log("- docs/ (entire directory)"); console.log('- docs/ (entire directory)');
if (analysis.epicFiles.length > 0) { if (analysis.epicFiles.length > 0) {
console.log( console.log(
chalk.green( chalk.green(
"\nNote: Epic files found! They will be placed in docs/prd/ with an index.md file." '\nNote: Epic files found! They will be placed in docs/prd/ with an index.md file.',
) ),
); );
console.log( console.log(
chalk.green( chalk.green("Since epic files exist, you won't need to shard the PRD after upgrade."),
"Since epic files exist, you won't need to shard the PRD after upgrade."
)
); );
} }
} }
} }
async createBackup(projectPath) { async createBackup(projectPath) {
const spinner = ora("Creating backup...").start(); const spinner = ora('Creating backup...').start();
try { try {
const backupPath = path.join(projectPath, ".bmad-v3-backup"); const backupPath = path.join(projectPath, '.bmad-v3-backup');
// Check if backup already exists // Check if backup already exists
if (await this.pathExists(backupPath)) { if (await this.pathExists(backupPath)) {
spinner.fail("Backup directory already exists"); spinner.fail('Backup directory already exists');
console.error( console.error(chalk.red('\nError: Backup directory .bmad-v3-backup/ already exists.'));
chalk.red( console.error('\nThis might mean an upgrade was already attempted.');
"\nError: Backup directory .bmad-v3-backup/ already exists." console.error('Please remove or rename the existing backup and try again.');
) throw new Error('Backup already exists');
);
console.error("\nThis might mean an upgrade was already attempted.");
console.error(
"Please remove or rename the existing backup and try again."
);
throw new Error("Backup already exists");
} }
// Create backup directory // Create backup directory
await fs.mkdir(backupPath, { recursive: true }); await fs.mkdir(backupPath, { recursive: true });
spinner.text = "✓ Created .bmad-v3-backup/"; spinner.text = '✓ Created .bmad-v3-backup/';
console.log(chalk.green("\n✓ Created .bmad-v3-backup/")); console.log(chalk.green('\n✓ Created .bmad-v3-backup/'));
// Move bmad-agent // Move bmad-agent
const bmadAgentSrc = path.join(projectPath, "bmad-agent"); const bmadAgentSource = path.join(projectPath, 'bmad-agent');
const bmadAgentDest = path.join(backupPath, "bmad-agent"); const bmadAgentDestination = path.join(backupPath, 'bmad-agent');
await fs.rename(bmadAgentSrc, bmadAgentDest); await fs.rename(bmadAgentSource, bmadAgentDestination);
console.log(chalk.green("✓ Moved bmad-agent/ to backup")); console.log(chalk.green('✓ Moved bmad-agent/ to backup'));
// Move docs // Move docs
const docsSrc = path.join(projectPath, "docs"); const docsSrc = path.join(projectPath, 'docs');
const docsDest = path.join(backupPath, "docs"); const docsDest = path.join(backupPath, 'docs');
await fs.rename(docsSrc, docsDest); await fs.rename(docsSrc, docsDest);
console.log(chalk.green("✓ Moved docs/ to backup")); console.log(chalk.green('✓ Moved docs/ to backup'));
spinner.succeed("Backup created successfully"); spinner.succeed('Backup created successfully');
} catch (error) { } catch (error) {
spinner.fail("Backup failed"); spinner.fail('Backup failed');
throw error; throw error;
} }
} }
async installV4Structure(projectPath) { async installV4Structure(projectPath) {
const spinner = ora("Installing V4 structure...").start(); const spinner = ora('Installing V4 structure...').start();
try { try {
// Get the source bmad-core directory (without dot prefix) // Get the source bmad-core directory (without dot prefix)
const sourcePath = path.join(__dirname, "..", "..", "bmad-core"); const sourcePath = path.join(__dirname, '..', '..', 'bmad-core');
const destPath = path.join(projectPath, ".bmad-core"); const destinationPath = path.join(projectPath, '.bmad-core');
// Copy .bmad-core // Copy .bmad-core
await this.copyDirectory(sourcePath, destPath); await this.copyDirectory(sourcePath, destinationPath);
spinner.text = "✓ Copied fresh .bmad-core/ directory from V4"; spinner.text = '✓ Copied fresh .bmad-core/ directory from V4';
console.log( console.log(chalk.green('\n✓ Copied fresh .bmad-core/ directory from V4'));
chalk.green("\n✓ Copied fresh .bmad-core/ directory from V4")
);
// Create docs directory // Create docs directory
const docsPath = path.join(projectPath, "docs"); const docsPath = path.join(projectPath, 'docs');
await fs.mkdir(docsPath, { recursive: true }); await fs.mkdir(docsPath, { recursive: true });
console.log(chalk.green("✓ Created new docs/ directory")); console.log(chalk.green('✓ Created new docs/ directory'));
// Create install manifest for future updates // Create install manifest for future updates
await this.createInstallManifest(projectPath); await this.createInstallManifest(projectPath);
console.log(chalk.green("✓ Created install manifest")); console.log(chalk.green('✓ Created install manifest'));
console.log( console.log(
chalk.yellow( chalk.yellow('\nNote: Your V3 bmad-agent content has been backed up and NOT migrated.'),
"\nNote: Your V3 bmad-agent content has been backed up and NOT migrated."
)
); );
console.log( console.log(
chalk.yellow( chalk.yellow(
"The new V4 agents are completely different and look for different file structures." 'The new V4 agents are completely different and look for different file structures.',
) ),
); );
spinner.succeed("V4 structure installed successfully"); spinner.succeed('V4 structure installed successfully');
} catch (error) { } catch (error) {
spinner.fail("V4 installation failed"); spinner.fail('V4 installation failed');
throw error; throw error;
} }
} }
async migrateDocuments(projectPath, analysis) { async migrateDocuments(projectPath, analysis) {
const spinner = ora("Migrating your project documents...").start(); const spinner = ora('Migrating your project documents...').start();
try { try {
const backupDocsPath = path.join(projectPath, ".bmad-v3-backup", "docs"); const backupDocsPath = path.join(projectPath, '.bmad-v3-backup', 'docs');
const newDocsPath = path.join(projectPath, "docs"); const newDocsPath = path.join(projectPath, 'docs');
let copiedCount = 0; let copiedCount = 0;
// Copy PRD // Copy PRD
if (analysis.prdFile) { if (analysis.prdFile) {
const src = path.join(backupDocsPath, analysis.prdFile); const source = path.join(backupDocsPath, analysis.prdFile);
const dest = path.join(newDocsPath, analysis.prdFile); const destination = path.join(newDocsPath, analysis.prdFile);
await fs.copyFile(src, dest); await fs.copyFile(source, destination);
console.log(chalk.green(`\n✓ Copied PRD to docs/${analysis.prdFile}`)); console.log(chalk.green(`\n✓ Copied PRD to docs/${analysis.prdFile}`));
copiedCount++; copiedCount++;
} }
// Copy Architecture // Copy Architecture
if (analysis.archFile) { if (analysis.archFile) {
const src = path.join(backupDocsPath, analysis.archFile); const source = path.join(backupDocsPath, analysis.archFile);
const dest = path.join(newDocsPath, analysis.archFile); const destination = path.join(newDocsPath, analysis.archFile);
await fs.copyFile(src, dest); await fs.copyFile(source, destination);
console.log( console.log(chalk.green(`✓ Copied Architecture to docs/${analysis.archFile}`));
chalk.green(`✓ Copied Architecture to docs/${analysis.archFile}`)
);
copiedCount++; copiedCount++;
} }
// Copy Front-end Architecture if exists // Copy Front-end Architecture if exists
if (analysis.frontEndArchFile) { if (analysis.frontEndArchFile) {
const src = path.join(backupDocsPath, analysis.frontEndArchFile); const source = path.join(backupDocsPath, analysis.frontEndArchFile);
const dest = path.join(newDocsPath, analysis.frontEndArchFile); const destination = path.join(newDocsPath, analysis.frontEndArchFile);
await fs.copyFile(src, dest); await fs.copyFile(source, destination);
console.log( console.log(
chalk.green( chalk.green(`✓ Copied Front-end Architecture to docs/${analysis.frontEndArchFile}`),
`✓ Copied Front-end Architecture to docs/${analysis.frontEndArchFile}`
)
); );
console.log( console.log(
chalk.yellow( chalk.yellow(
"Note: V4 uses a single full-stack-architecture.md - use doc-migration-task to merge" 'Note: V4 uses a single full-stack-architecture.md - use doc-migration-task to merge',
) ),
); );
copiedCount++; copiedCount++;
} }
// Copy UX/UI Spec if exists // Copy UX/UI Spec if exists
if (analysis.uxSpecFile) { if (analysis.uxSpecFile) {
const src = path.join(backupDocsPath, analysis.uxSpecFile); const source = path.join(backupDocsPath, analysis.uxSpecFile);
const dest = path.join(newDocsPath, analysis.uxSpecFile); const destination = path.join(newDocsPath, analysis.uxSpecFile);
await fs.copyFile(src, dest); await fs.copyFile(source, destination);
console.log( console.log(chalk.green(`✓ Copied UX/UI Spec to docs/${analysis.uxSpecFile}`));
chalk.green(`✓ Copied UX/UI Spec to docs/${analysis.uxSpecFile}`)
);
copiedCount++; copiedCount++;
} }
// Copy UX/Design Prompt if exists // Copy UX/Design Prompt if exists
if (analysis.uxPromptFile) { if (analysis.uxPromptFile) {
const src = path.join(backupDocsPath, analysis.uxPromptFile); const source = path.join(backupDocsPath, analysis.uxPromptFile);
const dest = path.join(newDocsPath, analysis.uxPromptFile); const destination = path.join(newDocsPath, analysis.uxPromptFile);
await fs.copyFile(src, dest); await fs.copyFile(source, destination);
console.log( console.log(chalk.green(`✓ Copied UX/Design Prompt to docs/${analysis.uxPromptFile}`));
chalk.green(
`✓ Copied UX/Design Prompt to docs/${analysis.uxPromptFile}`
)
);
copiedCount++; copiedCount++;
} }
// Copy stories // Copy stories
if (analysis.storyFiles.length > 0) { if (analysis.storyFiles.length > 0) {
const storiesDir = path.join(newDocsPath, "stories"); const storiesDir = path.join(newDocsPath, 'stories');
await fs.mkdir(storiesDir, { recursive: true }); await fs.mkdir(storiesDir, { recursive: true });
for (const storyFile of analysis.storyFiles) { for (const storyFile of analysis.storyFiles) {
const src = path.join(backupDocsPath, "stories", storyFile); const source = path.join(backupDocsPath, 'stories', storyFile);
const dest = path.join(storiesDir, storyFile); const destination = path.join(storiesDir, storyFile);
await fs.copyFile(src, dest); await fs.copyFile(source, destination);
} }
console.log( console.log(
chalk.green( chalk.green(`✓ Copied ${analysis.storyFiles.length} story files to docs/stories/`),
`✓ Copied ${analysis.storyFiles.length} story files to docs/stories/`
)
); );
copiedCount += analysis.storyFiles.length; copiedCount += analysis.storyFiles.length;
} }
// Copy epic files to prd subfolder // Copy epic files to prd subfolder
if (analysis.epicFiles.length > 0) { if (analysis.epicFiles.length > 0) {
const prdDir = path.join(newDocsPath, "prd"); const prdDir = path.join(newDocsPath, 'prd');
await fs.mkdir(prdDir, { recursive: true }); await fs.mkdir(prdDir, { recursive: true });
for (const epicFile of analysis.epicFiles) { for (const epicFile of analysis.epicFiles) {
const src = path.join(backupDocsPath, epicFile); const source = path.join(backupDocsPath, epicFile);
const dest = path.join(prdDir, epicFile); const destination = path.join(prdDir, epicFile);
await fs.copyFile(src, dest); await fs.copyFile(source, destination);
} }
console.log( console.log(
chalk.green( chalk.green(`✓ Found and copied ${analysis.epicFiles.length} epic files to docs/prd/`),
`✓ Found and copied ${analysis.epicFiles.length} epic files to docs/prd/`
)
); );
// Create index.md for the prd folder // Create index.md for the prd folder
await this.createPrdIndex(projectPath, analysis); await this.createPrdIndex(projectPath, analysis);
console.log(chalk.green("✓ Created index.md in docs/prd/")); console.log(chalk.green('✓ Created index.md in docs/prd/'));
console.log( console.log(
chalk.green( chalk.green(
"\nNote: Epic files detected! These are compatible with V4 and have been copied." '\nNote: Epic files detected! These are compatible with V4 and have been copied.',
) ),
);
console.log(
chalk.green(
"You won't need to shard the PRD since epics already exist."
)
); );
console.log(chalk.green("You won't need to shard the PRD since epics already exist."));
copiedCount += analysis.epicFiles.length; copiedCount += analysis.epicFiles.length;
} }
spinner.succeed(`Migrated ${copiedCount} documents successfully`); spinner.succeed(`Migrated ${copiedCount} documents successfully`);
} catch (error) { } catch (error) {
spinner.fail("Document migration failed"); spinner.fail('Document migration failed');
throw error; throw error;
} }
} }
@ -548,21 +481,21 @@ class V3ToV4Upgrader {
async setupIDE(projectPath, selectedIdes) { async setupIDE(projectPath, selectedIdes) {
// Use the IDE selections passed from the installer // Use the IDE selections passed from the installer
if (!selectedIdes || selectedIdes.length === 0) { if (!selectedIdes || selectedIdes.length === 0) {
console.log(chalk.dim("No IDE setup requested - skipping")); console.log(chalk.dim('No IDE setup requested - skipping'));
return; return;
} }
const ideSetup = require("../installer/lib/ide-setup"); const ideSetup = require('../installer/lib/ide-setup');
const spinner = ora("Setting up IDE rules for all agents...").start(); const spinner = ora('Setting up IDE rules for all agents...').start();
try { try {
const ideMessages = { const ideMessages = {
cursor: "Rules created in .cursor/rules/bmad/", cursor: 'Rules created in .cursor/rules/bmad/',
"claude-code": "Commands created in .claude/commands/BMad/", 'claude-code': 'Commands created in .claude/commands/BMad/',
windsurf: "Rules created in .windsurf/rules/", windsurf: 'Rules created in .windsurf/workflows/',
trae: "Rules created in.trae/rules/", trae: 'Rules created in.trae/rules/',
roo: "Custom modes created in .roomodes", roo: 'Custom modes created in .roomodes',
cline: "Rules created in .clinerules/", cline: 'Rules created in .clinerules/',
}; };
// Setup each selected IDE // Setup each selected IDE
@ -573,17 +506,15 @@ class V3ToV4Upgrader {
} }
spinner.succeed(`IDE setup complete for ${selectedIdes.length} IDE(s)!`); spinner.succeed(`IDE setup complete for ${selectedIdes.length} IDE(s)!`);
} catch (error) { } catch {
spinner.fail("IDE setup failed"); spinner.fail('IDE setup failed');
console.error( console.error(chalk.yellow('IDE setup failed, but upgrade is complete.'));
chalk.yellow("IDE setup failed, but upgrade is complete.")
);
} }
} }
showCompletionReport(projectPath, analysis) { showCompletionReport(projectPath, analysis) {
console.log(chalk.bold.green("\n✓ Upgrade Complete!\n")); console.log(chalk.bold.green('\n✓ Upgrade Complete!\n'));
console.log(chalk.bold("Summary:")); console.log(chalk.bold('Summary:'));
console.log(`- V3 files backed up to: .bmad-v3-backup/`); console.log(`- V3 files backed up to: .bmad-v3-backup/`);
console.log(`- V4 structure installed: .bmad-core/ (fresh from V4)`); console.log(`- V4 structure installed: .bmad-core/ (fresh from V4)`);
@ -596,50 +527,36 @@ class V3ToV4Upgrader {
analysis.storyFiles.length; analysis.storyFiles.length;
console.log( console.log(
`- Documents migrated: ${totalDocs} files${ `- Documents migrated: ${totalDocs} files${
analysis.epicFiles.length > 0 analysis.epicFiles.length > 0 ? ` + ${analysis.epicFiles.length} epics` : ''
? ` + ${analysis.epicFiles.length} epics` }`,
: ""
}`
); );
console.log(chalk.bold("\nImportant Changes:")); console.log(chalk.bold('\nImportant Changes:'));
console.log( console.log('- The V4 agents (sm, dev, etc.) expect different file structures than V3');
"- The V4 agents (sm, dev, etc.) expect different file structures than V3" console.log("- Your V3 bmad-agent content was NOT migrated (it's incompatible)");
);
console.log(
"- Your V3 bmad-agent content was NOT migrated (it's incompatible)"
);
if (analysis.epicFiles.length > 0) { if (analysis.epicFiles.length > 0) {
console.log( console.log('- Epic files were found and copied - no PRD sharding needed!');
"- Epic files were found and copied - no PRD sharding needed!"
);
} }
if (analysis.frontEndArchFile) { if (analysis.frontEndArchFile) {
console.log( console.log(
"- Front-end architecture found - V4 uses full-stack-architecture.md, migration needed" '- Front-end architecture found - V4 uses full-stack-architecture.md, migration needed',
); );
} }
if (analysis.uxSpecFile || analysis.uxPromptFile) { if (analysis.uxSpecFile || analysis.uxPromptFile) {
console.log( console.log('- UX/UI design files found and copied - ready for use with V4');
"- UX/UI design files found and copied - ready for use with V4"
);
} }
console.log(chalk.bold("\nNext Steps:")); console.log(chalk.bold('\nNext Steps:'));
console.log("1. Review your documents in the new docs/ folder"); console.log('1. Review your documents in the new docs/ folder');
console.log( console.log(
"2. Use @bmad-master agent to run the doc-migration-task to align your documents with V4 templates" '2. Use @bmad-master agent to run the doc-migration-task to align your documents with V4 templates',
); );
if (analysis.epicFiles.length === 0) { if (analysis.epicFiles.length === 0) {
console.log( console.log('3. Use @bmad-master agent to shard the PRD to create epic files');
"3. Use @bmad-master agent to shard the PRD to create epic files"
);
} }
console.log( console.log(
chalk.dim( chalk.dim('\nYour V3 backup is preserved in .bmad-v3-backup/ and can be restored if needed.'),
"\nYour V3 backup is preserved in .bmad-v3-backup/ and can be restored if needed."
)
); );
} }
@ -652,67 +569,61 @@ class V3ToV4Upgrader {
} }
} }
async copyDirectory(src, dest) { async copyDirectory(source, destination) {
await fs.mkdir(dest, { recursive: true }); await fs.mkdir(destination, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true }); const entries = await fs.readdir(source, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
const srcPath = path.join(src, entry.name); const sourcePath = path.join(source, entry.name);
const destPath = path.join(dest, entry.name); const destinationPath = path.join(destination, entry.name);
if (entry.isDirectory()) { await (entry.isDirectory()
await this.copyDirectory(srcPath, destPath); ? this.copyDirectory(sourcePath, destinationPath)
} else { : fs.copyFile(sourcePath, destinationPath));
await fs.copyFile(srcPath, destPath);
}
} }
} }
async createPrdIndex(projectPath, analysis) { async createPrdIndex(projectPath, analysis) {
const prdIndexPath = path.join(projectPath, "docs", "prd", "index.md"); const prdIndexPath = path.join(projectPath, 'docs', 'prd', 'index.md');
const prdPath = path.join( const prdPath = path.join(projectPath, 'docs', analysis.prdFile || 'prd.md');
projectPath,
"docs",
analysis.prdFile || "prd.md"
);
let indexContent = "# Product Requirements Document\n\n"; let indexContent = '# Product Requirements Document\n\n';
// Try to read the PRD to get the title and intro content // Try to read the PRD to get the title and intro content
if (analysis.prdFile && (await this.pathExists(prdPath))) { if (analysis.prdFile && (await this.pathExists(prdPath))) {
try { try {
const prdContent = await fs.readFile(prdPath, "utf8"); const prdContent = await fs.readFile(prdPath, 'utf8');
const lines = prdContent.split("\n"); const lines = prdContent.split('\n');
// Find the first heading // Find the first heading
const titleMatch = lines.find((line) => line.startsWith("# ")); const titleMatch = lines.find((line) => line.startsWith('# '));
if (titleMatch) { if (titleMatch) {
indexContent = titleMatch + "\n\n"; indexContent = titleMatch + '\n\n';
} }
// Get any content before the first ## section // Get any content before the first ## section
let introContent = ""; let introContent = '';
let foundFirstSection = false; let foundFirstSection = false;
for (const line of lines) { for (const line of lines) {
if (line.startsWith("## ")) { if (line.startsWith('## ')) {
foundFirstSection = true; foundFirstSection = true;
break; break;
} }
if (!line.startsWith("# ")) { if (!line.startsWith('# ')) {
introContent += line + "\n"; introContent += line + '\n';
} }
} }
if (introContent.trim()) { if (introContent.trim()) {
indexContent += introContent.trim() + "\n\n"; indexContent += introContent.trim() + '\n\n';
} }
} catch (error) { } catch {
// If we can't read the PRD, just use default content // If we can't read the PRD, just use default content
} }
} }
// Add sections list // Add sections list
indexContent += "## Sections\n\n"; indexContent += '## Sections\n\n';
// Sort epic files for consistent ordering // Sort epic files for consistent ordering
const sortedEpics = [...analysis.epicFiles].sort(); const sortedEpics = [...analysis.epicFiles].sort();
@ -720,38 +631,36 @@ class V3ToV4Upgrader {
for (const epicFile of sortedEpics) { for (const epicFile of sortedEpics) {
// Extract epic name from filename // Extract epic name from filename
const epicName = epicFile const epicName = epicFile
.replace(/\.md$/, "") .replace(/\.md$/, '')
.replace(/^epic-?/i, "") .replace(/^epic-?/i, '')
.replace(/-/g, " ") .replaceAll('-', ' ')
.replace(/^\d+\s*/, "") // Remove leading numbers .replace(/^\d+\s*/, '') // Remove leading numbers
.trim(); .trim();
const displayName = epicName.charAt(0).toUpperCase() + epicName.slice(1); const displayName = epicName.charAt(0).toUpperCase() + epicName.slice(1);
indexContent += `- [${ indexContent += `- [${displayName || epicFile.replace('.md', '')}](./${epicFile})\n`;
displayName || epicFile.replace(".md", "")
}](./${epicFile})\n`;
} }
await fs.writeFile(prdIndexPath, indexContent); await fs.writeFile(prdIndexPath, indexContent);
} }
async createInstallManifest(projectPath) { async createInstallManifest(projectPath) {
const fileManager = require("../installer/lib/file-manager"); const fileManager = require('../installer/lib/file-manager');
const { glob } = require("glob"); const { glob } = require('glob');
// Get all files in .bmad-core for the manifest // Get all files in .bmad-core for the manifest
const bmadCorePath = path.join(projectPath, ".bmad-core"); const bmadCorePath = path.join(projectPath, '.bmad-core');
const files = await glob("**/*", { const files = await glob('**/*', {
cwd: bmadCorePath, cwd: bmadCorePath,
nodir: true, nodir: true,
ignore: ["**/.git/**", "**/node_modules/**"], ignore: ['**/.git/**', '**/node_modules/**'],
}); });
// Prepend .bmad-core/ to file paths for manifest // Prepend .bmad-core/ to file paths for manifest
const manifestFiles = files.map((file) => path.join(".bmad-core", file)); const manifestFiles = files.map((file) => path.join('.bmad-core', file));
const config = { const config = {
installType: "full", installType: 'full',
agent: null, agent: null,
ide: null, // Will be set if IDE setup is done later ide: null, // Will be set if IDE setup is done later
}; };

View File

@ -1,8 +1,6 @@
#!/usr/bin/env node const fs = require('node:fs');
const { execSync } = require('node:child_process');
const fs = require('fs'); const path = require('node:path');
const { execSync } = require('child_process');
const path = require('path');
// Dynamic import for ES module // Dynamic import for ES module
let chalk; let chalk;
@ -58,7 +56,7 @@ async function main() {
// Check if working directory is clean // Check if working directory is clean
try { try {
execSync('git diff-index --quiet HEAD --'); execSync('git diff-index --quiet HEAD --');
} catch (error) { } catch {
console.error(chalk.red('❌ Working directory is not clean. Commit your changes first.')); console.error(chalk.red('❌ Working directory is not clean. Commit your changes first.'));
process.exit(1); process.exit(1);
} }
@ -70,7 +68,7 @@ async function main() {
} }
if (require.main === module) { if (require.main === module) {
main().catch(error => { main().catch((error) => {
console.error('Error:', error); console.error('Error:', error);
process.exit(1); process.exit(1);
}); });

View File

@ -1,9 +1,7 @@
#!/usr/bin/env node const fs = require('node:fs');
const path = require('node:path');
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const { execSync } = require('child_process'); const { execSync } = require('node:child_process');
// Dynamic import for ES module // Dynamic import for ES module
let chalk; let chalk;
@ -26,25 +24,32 @@ async function formatYamlContent(content, filename) {
// First try to fix common YAML issues // First try to fix common YAML issues
let fixedContent = content let fixedContent = content
// Fix "commands :" -> "commands:" // Fix "commands :" -> "commands:"
.replace(/^(\s*)(\w+)\s+:/gm, '$1$2:') .replaceAll(/^(\s*)(\w+)\s+:/gm, '$1$2:')
// Fix inconsistent list indentation // Fix inconsistent list indentation
.replace(/^(\s*)-\s{3,}/gm, '$1- '); .replaceAll(/^(\s*)-\s{3,}/gm, '$1- ');
// Skip auto-fixing for .roomodes files - they have special nested structure // Skip auto-fixing for .roomodes files - they have special nested structure
if (!filename.includes('.roomodes')) { if (!filename.includes('.roomodes')) {
fixedContent = fixedContent fixedContent = fixedContent
// Fix unquoted list items that contain special characters or multiple parts // Fix unquoted list items that contain special characters or multiple parts
.replace(/^(\s*)-\s+(.*)$/gm, (match, indent, content) => { .replaceAll(/^(\s*)-\s+(.*)$/gm, (match, indent, content) => {
// Skip if already quoted // Skip if already quoted
if (content.startsWith('"') && content.endsWith('"')) { if (content.startsWith('"') && content.endsWith('"')) {
return match; return match;
} }
// If the content contains special YAML characters or looks complex, quote it // If the content contains special YAML characters or looks complex, quote it
// BUT skip if it looks like a proper YAML key-value pair (like "key: value") // BUT skip if it looks like a proper YAML key-value pair (like "key: value")
if ((content.includes(':') || content.includes('-') || content.includes('{') || content.includes('}')) && if (
!content.match(/^\w+:\s/)) { (content.includes(':') ||
content.includes('-') ||
content.includes('{') ||
content.includes('}')) &&
!/^\w+:\s/.test(content)
) {
// Remove any existing quotes first, escape internal quotes, then add proper quotes // Remove any existing quotes first, escape internal quotes, then add proper quotes
const cleanContent = content.replace(/^["']|["']$/g, '').replace(/"/g, '\\"'); const cleanContent = content
.replaceAll(/^["']|["']$/g, '')
.replaceAll('"', String.raw`\"`);
return `${indent}- "${cleanContent}"`; return `${indent}- "${cleanContent}"`;
} }
return match; return match;
@ -62,7 +67,7 @@ async function formatYamlContent(content, filename) {
indent: 2, indent: 2,
lineWidth: -1, // Disable line wrapping lineWidth: -1, // Disable line wrapping
noRefs: true, noRefs: true,
sortKeys: false // Preserve key order sortKeys: false, // Preserve key order
}); });
return formatted; return formatted;
} catch (error) { } catch (error) {
@ -80,7 +85,7 @@ async function processMarkdownFile(filePath) {
// Fix untyped code blocks by adding 'text' type // Fix untyped code blocks by adding 'text' type
// Match ``` at start of line followed by newline, but only if it's an opening fence // Match ``` at start of line followed by newline, but only if it's an opening fence
newContent = newContent.replace(/^```\n([\s\S]*?)\n```$/gm, '```text\n$1\n```'); newContent = newContent.replaceAll(/^```\n([\s\S]*?)\n```$/gm, '```text\n$1\n```');
if (newContent !== content) { if (newContent !== content) {
modified = true; modified = true;
console.log(chalk.blue(`🔧 Added 'text' type to untyped code blocks in ${filePath}`)); console.log(chalk.blue(`🔧 Added 'text' type to untyped code blocks in ${filePath}`));
@ -106,14 +111,14 @@ async function processMarkdownFile(filePath) {
replacements.push({ replacements.push({
start: match.index, start: match.index,
end: match.index + fullMatch.length, end: match.index + fullMatch.length,
replacement: `\`\`\`yaml\n${trimmedFormatted}\n\`\`\`` replacement: `\`\`\`yaml\n${trimmedFormatted}\n\`\`\``,
}); });
} }
} }
// Apply replacements in reverse order to maintain indices // Apply replacements in reverse order to maintain indices
for (let i = replacements.length - 1; i >= 0; i--) { for (let index = replacements.length - 1; index >= 0; index--) {
const { start, end, replacement } = replacements[i]; const { start, end, replacement } = replacements[index];
newContent = newContent.slice(0, start) + replacement + newContent.slice(end); newContent = newContent.slice(0, start) + replacement + newContent.slice(end);
} }
@ -155,10 +160,10 @@ async function lintYamlFile(filePath) {
async function main() { async function main() {
await initializeModules(); await initializeModules();
const args = process.argv.slice(2); const arguments_ = process.argv.slice(2);
const glob = require('glob'); const glob = require('glob');
if (args.length === 0) { if (arguments_.length === 0) {
console.error('Usage: node yaml-format.js <file1> [file2] ...'); console.error('Usage: node yaml-format.js <file1> [file2] ...');
process.exit(1); process.exit(1);
} }
@ -169,35 +174,41 @@ async function main() {
// Expand glob patterns and collect all files // Expand glob patterns and collect all files
const allFiles = []; const allFiles = [];
for (const arg of args) { for (const argument of arguments_) {
if (arg.includes('*')) { if (argument.includes('*')) {
// It's a glob pattern // It's a glob pattern
const matches = glob.sync(arg); const matches = glob.sync(argument);
allFiles.push(...matches); allFiles.push(...matches);
} else { } else {
// It's a direct file path // It's a direct file path
allFiles.push(arg); allFiles.push(argument);
} }
} }
for (const filePath of allFiles) { for (const filePath of allFiles) {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
// Skip silently for glob patterns that don't match anything // Skip silently for glob patterns that don't match anything
if (!args.some(arg => arg.includes('*') && filePath === arg)) { if (!arguments_.some((argument) => argument.includes('*') && filePath === argument)) {
console.error(chalk.red(`❌ File not found: ${filePath}`)); console.error(chalk.red(`❌ File not found: ${filePath}`));
hasErrors = true; hasErrors = true;
} }
continue; continue;
} }
const ext = path.extname(filePath).toLowerCase(); const extension = path.extname(filePath).toLowerCase();
const basename = path.basename(filePath).toLowerCase(); const basename = path.basename(filePath).toLowerCase();
try { try {
let changed = false; let changed = false;
if (ext === '.md') { if (extension === '.md') {
changed = await processMarkdownFile(filePath); changed = await processMarkdownFile(filePath);
} else if (ext === '.yaml' || ext === '.yml' || basename.includes('roomodes') || basename.includes('.yaml') || basename.includes('.yml')) { } else if (
extension === '.yaml' ||
extension === '.yml' ||
basename.includes('roomodes') ||
basename.includes('.yaml') ||
basename.includes('.yml')
) {
// Handle YAML files and special cases like .roomodes // Handle YAML files and special cases like .roomodes
changed = await processYamlFile(filePath); changed = await processYamlFile(filePath);
@ -220,8 +231,10 @@ async function main() {
} }
if (hasChanges) { if (hasChanges) {
console.log(chalk.green(`\n✨ YAML formatting completed! Modified ${filesProcessed.length} files:`)); console.log(
filesProcessed.forEach(file => console.log(chalk.blue(` 📝 ${file}`))); chalk.green(`\n✨ YAML formatting completed! Modified ${filesProcessed.length} files:`),
);
for (const file of filesProcessed) console.log(chalk.blue(` 📝 ${file}`));
} }
if (hasErrors) { if (hasErrors) {
@ -231,7 +244,7 @@ async function main() {
} }
if (require.main === module) { if (require.main === module) {
main().catch(error => { main().catch((error) => {
console.error('Error:', error); console.error('Error:', error);
process.exit(1); process.exit(1);
}); });