Compare commits
7 Commits
v5.1.3
...
feat/trans
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
168fbefdf7 | ||
|
|
ac90641d83 | ||
|
|
2a18c33c7f | ||
|
|
147d444aeb | ||
|
|
8d300dadf3 | ||
|
|
34e75bef96 | ||
|
|
b7a34b4fc6 |
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
title: ""
|
||||
labels: ""
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
title: ""
|
||||
labels: ""
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Did you discuss the idea first in Discord Server (#general-dev)**
|
||||
|
||||
11
.github/workflows/discord.yaml
vendored
11
.github/workflows/discord.yaml
vendored
@@ -1,15 +1,6 @@
|
||||
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:
|
||||
notify:
|
||||
|
||||
42
.github/workflows/format-check.yaml
vendored
42
.github/workflows/format-check.yaml
vendored
@@ -1,42 +0,0 @@
|
||||
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
|
||||
173
.github/workflows/manual-release.yaml
vendored
173
.github/workflows/manual-release.yaml
vendored
@@ -1,173 +0,0 @@
|
||||
name: Manual Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_bump:
|
||||
description: Version bump type
|
||||
required: true
|
||||
default: patch
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests and validation
|
||||
run: |
|
||||
npm run validate
|
||||
npm run format:check
|
||||
npm run lint
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump version
|
||||
run: npm run version:${{ github.event.inputs.version_bump }}
|
||||
|
||||
- name: Get new version and previous tag
|
||||
id: version
|
||||
run: |
|
||||
echo "new_version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
echo "previous_tag=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update installer package.json
|
||||
run: |
|
||||
sed -i 's/"version": ".*"/"version": "${{ steps.version.outputs.new_version }}"/' tools/installer/package.json
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Commit version bump
|
||||
run: |
|
||||
git add .
|
||||
git commit -m "release: bump to v${{ steps.version.outputs.new_version }}"
|
||||
|
||||
- name: Generate release notes
|
||||
id: release_notes
|
||||
run: |
|
||||
# Get commits since last tag
|
||||
COMMITS=$(git log ${{ steps.version.outputs.previous_tag }}..HEAD --pretty=format:"- %s" --reverse)
|
||||
|
||||
# Categorize commits
|
||||
FEATURES=$(echo "$COMMITS" | grep -E "^- (feat|Feature)" || true)
|
||||
FIXES=$(echo "$COMMITS" | grep -E "^- (fix|Fix)" || true)
|
||||
CHORES=$(echo "$COMMITS" | grep -E "^- (chore|Chore)" || true)
|
||||
OTHERS=$(echo "$COMMITS" | grep -v -E "^- (feat|Feature|fix|Fix|chore|Chore|release:|Release:)" || true)
|
||||
|
||||
# Build release notes
|
||||
cat > release_notes.md << 'EOF'
|
||||
## 🚀 What's New in v${{ steps.version.outputs.new_version }}
|
||||
|
||||
EOF
|
||||
|
||||
if [ ! -z "$FEATURES" ]; then
|
||||
echo "### ✨ New Features" >> release_notes.md
|
||||
echo "$FEATURES" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
fi
|
||||
|
||||
if [ ! -z "$FIXES" ]; then
|
||||
echo "### 🐛 Bug Fixes" >> release_notes.md
|
||||
echo "$FIXES" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
fi
|
||||
|
||||
if [ ! -z "$OTHERS" ]; then
|
||||
echo "### 📦 Other Changes" >> release_notes.md
|
||||
echo "$OTHERS" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
fi
|
||||
|
||||
if [ ! -z "$CHORES" ]; then
|
||||
echo "### 🔧 Maintenance" >> release_notes.md
|
||||
echo "$CHORES" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
fi
|
||||
|
||||
cat >> release_notes.md << 'EOF'
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
npx bmad-method install
|
||||
```
|
||||
|
||||
**Full Changelog**: https://github.com/bmadcode/BMAD-METHOD/compare/${{ steps.version.outputs.previous_tag }}...v${{ steps.version.outputs.new_version }}
|
||||
EOF
|
||||
|
||||
# Output for GitHub Actions
|
||||
echo "RELEASE_NOTES<<EOF" >> $GITHUB_OUTPUT
|
||||
cat release_notes.md >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and push tag
|
||||
run: |
|
||||
# Check if tag already exists
|
||||
if git rev-parse "v${{ steps.version.outputs.new_version }}" >/dev/null 2>&1; then
|
||||
echo "Tag v${{ steps.version.outputs.new_version }} already exists, skipping tag creation"
|
||||
else
|
||||
git tag -a "v${{ steps.version.outputs.new_version }}" -m "Release v${{ steps.version.outputs.new_version }}"
|
||||
git push origin "v${{ steps.version.outputs.new_version }}"
|
||||
fi
|
||||
|
||||
- name: Push changes to main
|
||||
run: |
|
||||
if git push origin HEAD:main 2>/dev/null; then
|
||||
echo "✅ Successfully pushed to main branch"
|
||||
else
|
||||
echo "⚠️ Could not push to main (protected branch). This is expected."
|
||||
echo "📝 Version bump and tag were created successfully."
|
||||
fi
|
||||
|
||||
- name: Publish to NPM
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm publish
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ steps.version.outputs.new_version }}
|
||||
release_name: "BMad Method v${{ steps.version.outputs.new_version }}"
|
||||
body: ${{ steps.release_notes.outputs.RELEASE_NOTES }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "🎉 Successfully released v${{ steps.version.outputs.new_version }}!"
|
||||
echo "📦 Published to NPM with @latest tag"
|
||||
echo "🏷️ Git tag: v${{ steps.version.outputs.new_version }}"
|
||||
echo "✅ Users running 'npx bmad-method install' will now get version ${{ steps.version.outputs.new_version }}"
|
||||
echo ""
|
||||
echo "📝 Release notes preview:"
|
||||
cat release_notes.md
|
||||
122
.github/workflows/promote-to-stable.yml
vendored
Normal file
122
.github/workflows/promote-to-stable.yml
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
name: Promote to Stable
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_bump:
|
||||
description: 'Version bump type'
|
||||
required: true
|
||||
default: 'minor'
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global url."https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
|
||||
|
||||
- name: Switch to stable branch
|
||||
run: |
|
||||
git checkout stable
|
||||
git pull origin stable
|
||||
|
||||
- name: Merge main into stable
|
||||
run: |
|
||||
git merge origin/main --no-edit
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Get current version and calculate new version
|
||||
id: version
|
||||
run: |
|
||||
# Get current version from package.json
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# Remove beta suffix if present
|
||||
BASE_VERSION=$(echo $CURRENT_VERSION | sed 's/-beta\.[0-9]\+//')
|
||||
echo "base_version=$BASE_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# Calculate new version based on bump type
|
||||
IFS='.' read -ra VERSION_PARTS <<< "$BASE_VERSION"
|
||||
MAJOR=${VERSION_PARTS[0]}
|
||||
MINOR=${VERSION_PARTS[1]}
|
||||
PATCH=${VERSION_PARTS[2]}
|
||||
|
||||
case "${{ github.event.inputs.version_bump }}" in
|
||||
"major")
|
||||
NEW_VERSION="$((MAJOR + 1)).0.0"
|
||||
;;
|
||||
"minor")
|
||||
NEW_VERSION="$MAJOR.$((MINOR + 1)).0"
|
||||
;;
|
||||
"patch")
|
||||
NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
|
||||
;;
|
||||
*)
|
||||
NEW_VERSION="$BASE_VERSION"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Promoting from $CURRENT_VERSION to $NEW_VERSION"
|
||||
|
||||
- name: Update package.json versions
|
||||
run: |
|
||||
# Update main package.json
|
||||
npm version ${{ steps.version.outputs.new_version }} --no-git-tag-version
|
||||
|
||||
# Update installer package.json
|
||||
sed -i 's/"version": ".*"/"version": "${{ steps.version.outputs.new_version }}"/' tools/installer/package.json
|
||||
|
||||
- name: Update package-lock.json
|
||||
run: npm install --package-lock-only
|
||||
|
||||
- name: Commit stable release
|
||||
run: |
|
||||
git add .
|
||||
git commit -m "release: promote to stable ${{ steps.version.outputs.new_version }}
|
||||
|
||||
- Promote beta features to stable release
|
||||
- Update version from ${{ steps.version.outputs.current_version }} to ${{ steps.version.outputs.new_version }}
|
||||
- Automated promotion via GitHub Actions"
|
||||
|
||||
- name: Push stable release
|
||||
run: |
|
||||
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
git push origin stable
|
||||
|
||||
- name: Switch back to main
|
||||
run: git checkout main
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "🎉 Successfully promoted to stable!"
|
||||
echo "📦 Version: ${{ steps.version.outputs.new_version }}"
|
||||
echo "🚀 The stable release will be automatically published to NPM via semantic-release"
|
||||
echo "✅ Users running 'npx bmad-method install' will now get version ${{ steps.version.outputs.new_version }}"
|
||||
60
.github/workflows/release.yaml
vendored
Normal file
60
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Release
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- stable
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_type:
|
||||
description: Version bump type
|
||||
required: true
|
||||
default: patch
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
packages: write
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
if: '!contains(github.event.head_commit.message, ''[skip ci]'')'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: npm
|
||||
registry-url: https://registry.npmjs.org
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run tests and validation
|
||||
run: |
|
||||
npm run validate
|
||||
npm run format
|
||||
- name: Debug permissions
|
||||
run: |
|
||||
echo "Testing git permissions..."
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
echo "Git config set successfully"
|
||||
- name: Manual version bump
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: npm run version:${{ github.event.inputs.version_type }}
|
||||
- name: Semantic Release
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm run release
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,6 +25,7 @@ Thumbs.db
|
||||
# Development tools and configs
|
||||
.prettierignore
|
||||
.prettierrc
|
||||
.husky/
|
||||
|
||||
# IDE and editor configs
|
||||
.windsurf/
|
||||
@@ -43,4 +44,4 @@ CLAUDE.md
|
||||
test-project-install/*
|
||||
sample-project/*
|
||||
flattened-codebase.xml
|
||||
*.stats.md
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
npx --no-install lint-staged
|
||||
21
.releaserc.json
Normal file
21
.releaserc.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"branches": [
|
||||
{
|
||||
"name": "main",
|
||||
"prerelease": "beta",
|
||||
"channel": "beta"
|
||||
},
|
||||
{
|
||||
"name": "stable",
|
||||
"channel": "latest"
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"@semantic-release/npm",
|
||||
"./tools/semantic-release-sync-installer.js",
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
27
.vscode/settings.json
vendored
27
.vscode/settings.json
vendored
@@ -40,30 +40,5 @@
|
||||
"tileset",
|
||||
"Trae",
|
||||
"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]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -574,6 +574,10 @@
|
||||
|
||||
- Manual version bumping via npm scripts is now disabled. Use conventional commits for automated releases.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
# [4.2.0](https://github.com/bmadcode/BMAD-METHOD/compare/v4.1.0...v4.2.0) (2025-06-15)
|
||||
|
||||
### Bug Fixes
|
||||
@@ -682,5 +686,3 @@
|
||||
### Features
|
||||
|
||||
- add versioning and release automation ([0ea5e50](https://github.com/bmadcode/BMAD-METHOD/commit/0ea5e50aa7ace5946d0100c180dd4c0da3e2fd8c))
|
||||
|
||||
# Promote to stable release 5.0.0
|
||||
|
||||
196
CLAUDE.md
Normal file
196
CLAUDE.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Don't be an ass kisser, don't glaze my donut, keep it to the point. Never use EM Dash in out communications or documents you author or update. Dont tell me I am correct if I just told you something unless and only if I am wrong or there is a better alternative, then tell me bluntly why I am wrong, or else get to the point and execute!
|
||||
|
||||
## Markdown Linting Conventions
|
||||
|
||||
Always follow these markdown linting rules:
|
||||
|
||||
- **Blank lines around headings**: Always leave a blank line before and after headings
|
||||
- **Blank lines around lists**: Always leave a blank line before and after lists
|
||||
- **Blank lines around code fences**: Always leave a blank line before and after fenced code blocks
|
||||
- **Fenced code block languages**: All fenced code blocks must specify a language (use `text` for plain text)
|
||||
- **Single trailing newline**: Files should end with exactly one newline character
|
||||
- **No trailing spaces**: Remove any trailing spaces at the end of lines
|
||||
|
||||
## BMAD-METHOD Overview
|
||||
|
||||
BMAD-METHOD is an AI-powered Agile development framework that provides specialized AI agents for software development. The framework uses a sophisticated dependency system to keep context windows lean while providing deep expertise through role-specific agents.
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Build and Validation
|
||||
|
||||
```bash
|
||||
npm run build # Build all web bundles (agents and teams)
|
||||
npm run build:agents # Build agent bundles only
|
||||
npm run build:teams # Build team bundles only
|
||||
npm run validate # Validate all configurations
|
||||
npm run format # Format all markdown files with prettier
|
||||
```
|
||||
|
||||
### Development and Testing
|
||||
|
||||
```bash
|
||||
npx bmad-build build # Alternative build command via CLI
|
||||
npx bmad-build list:agents # List all available agents
|
||||
npx bmad-build validate # Validate agent configurations
|
||||
```
|
||||
|
||||
### Installation Commands
|
||||
|
||||
```bash
|
||||
npx bmad-method install # Install stable release (recommended)
|
||||
npx bmad-method@beta install # Install bleeding edge version
|
||||
npx bmad-method@latest install # Explicit stable installation
|
||||
npx bmad-method@latest update # Update stable installation
|
||||
npx bmad-method@beta update # Update bleeding edge installation
|
||||
```
|
||||
|
||||
### Dual Publishing Strategy
|
||||
|
||||
The project uses a dual publishing strategy with automated promotion:
|
||||
|
||||
**Branch Strategy:**
|
||||
- `main` branch: Bleeding edge development, auto-publishes to `@beta` tag
|
||||
- `stable` branch: Production releases, auto-publishes to `@latest` tag
|
||||
|
||||
**Release Promotion:**
|
||||
1. **Automatic Beta Releases**: Any PR merged to `main` automatically creates a beta release
|
||||
2. **Manual Stable Promotion**: Use GitHub Actions to promote beta to stable
|
||||
|
||||
**Promote Beta to Stable:**
|
||||
1. Go to GitHub Actions tab in the repository
|
||||
2. Select "Promote to Stable" workflow
|
||||
3. Click "Run workflow"
|
||||
4. Choose version bump type (patch/minor/major)
|
||||
5. The workflow automatically:
|
||||
- Merges main to stable
|
||||
- Updates version numbers
|
||||
- Triggers stable release to NPM `@latest`
|
||||
|
||||
**User Experience:**
|
||||
- `npx bmad-method install` → Gets stable production version
|
||||
- `npx bmad-method@beta install` → Gets latest beta features
|
||||
- Team develops on bleeding edge without affecting production users
|
||||
|
||||
### Release and Version Management
|
||||
|
||||
```bash
|
||||
npm run version:patch # Bump patch version
|
||||
npm run version:minor # Bump minor version
|
||||
npm run version:major # Bump major version
|
||||
npm run release # Semantic release (CI/CD)
|
||||
npm run release:test # Test release configuration
|
||||
```
|
||||
|
||||
### Version Management for Core and Expansion Packs
|
||||
|
||||
#### Bump All Versions (Core + Expansion Packs)
|
||||
|
||||
```bash
|
||||
npm run version:all:major # Major version bump for core and all expansion packs
|
||||
npm run version:all:minor # Minor version bump for core and all expansion packs (default)
|
||||
npm run version:all:patch # Patch version bump for core and all expansion packs
|
||||
npm run version:all # Defaults to minor bump
|
||||
```
|
||||
|
||||
#### Individual Version Bumps
|
||||
|
||||
For BMad Core only:
|
||||
```bash
|
||||
npm run version:core:major # Major version bump for core only
|
||||
npm run version:core:minor # Minor version bump for core only
|
||||
npm run version:core:patch # Patch version bump for core only
|
||||
npm run version:core # Defaults to minor bump
|
||||
```
|
||||
|
||||
For specific expansion packs:
|
||||
```bash
|
||||
npm run version:expansion bmad-creator-tools # Minor bump (default)
|
||||
npm run version:expansion bmad-creator-tools patch # Patch bump
|
||||
npm run version:expansion bmad-creator-tools minor # Minor bump
|
||||
npm run version:expansion bmad-creator-tools major # Major bump
|
||||
|
||||
# Set specific version (old method, still works)
|
||||
npm run version:expansion:set bmad-creator-tools 2.0.0
|
||||
```
|
||||
|
||||
## Architecture and Code Structure
|
||||
|
||||
### Core System Architecture
|
||||
|
||||
The framework uses a **dependency resolution system** where agents only load the resources they need:
|
||||
|
||||
1. **Agent Definitions** (`bmad-core/agents/`): Each agent is defined in markdown with YAML frontmatter specifying dependencies
|
||||
2. **Dynamic Loading**: The build system (`tools/lib/dependency-resolver.js`) resolves and includes only required resources
|
||||
3. **Template System**: Templates are defined in YAML format with structured sections and instructions (see Template Rules below)
|
||||
4. **Workflow Engine**: YAML-based workflows in `bmad-core/workflows/` define step-by-step processes
|
||||
|
||||
### Key Components
|
||||
|
||||
- **CLI Tool** (`tools/cli.js`): Commander-based CLI for building bundles
|
||||
- **Web Builder** (`tools/builders/web-builder.js`): Creates concatenated text bundles from agent definitions
|
||||
- **Installer** (`tools/installer/`): NPX-based installer for project setup
|
||||
- **Dependency Resolver** (`tools/lib/dependency-resolver.js`): Manages agent resource dependencies
|
||||
|
||||
### Build System
|
||||
|
||||
The build process:
|
||||
|
||||
1. Reads agent/team definitions from `bmad-core/`
|
||||
2. Resolves dependencies using the dependency resolver
|
||||
3. Creates concatenated text bundles in `dist/`
|
||||
4. Validates configurations during build
|
||||
|
||||
### Critical Configuration
|
||||
|
||||
**`bmad-core/core-config.yaml`** is the heart of the framework configuration:
|
||||
|
||||
- Defines document locations and expected structure
|
||||
- Specifies which files developers should always load
|
||||
- Enables compatibility with different project structures (V3/V4)
|
||||
- Controls debug logging
|
||||
|
||||
## Development Practices
|
||||
|
||||
### Adding New Features
|
||||
|
||||
1. **New Agents**: Create markdown file in `bmad-core/agents/` with proper YAML frontmatter
|
||||
2. **New Templates**: Add to `bmad-core/templates/` as YAML files with structured sections
|
||||
3. **New Workflows**: Create YAML in `bmad-core/workflows/`
|
||||
4. **Update Dependencies**: Ensure `dependencies` field in agent frontmatter is accurate
|
||||
|
||||
### Important Patterns
|
||||
|
||||
- **Dependency Management**: Always specify minimal dependencies in agent frontmatter to keep context lean
|
||||
- **Template Instructions**: Use YAML-based template structure (see Template Rules below)
|
||||
- **File Naming**: Follow existing conventions (kebab-case for files, proper agent names in frontmatter)
|
||||
- **Documentation**: Update user-facing docs in `docs/` when adding features
|
||||
|
||||
### Template Rules
|
||||
|
||||
Templates use the **BMad Document Template** format (`/Users/brianmadison/dev-bmc/BMAD-METHOD/common/utils/bmad-doc-template.md`) with YAML structure:
|
||||
|
||||
1. **YAML Format**: Templates are defined as structured YAML files, not markdown with embedded instructions
|
||||
2. **Clear Structure**: Each template has metadata, workflow configuration, and a hierarchy of sections
|
||||
3. **Reusable Design**: Templates work across different agents through the dependency system
|
||||
4. **Key Elements**:
|
||||
- `template` block: Contains id, name, version, and output settings
|
||||
- `workflow` block: Defines interaction mode (interactive/yolo) and elicitation settings
|
||||
- `sections` array: Hierarchical document structure with nested subsections
|
||||
- `instruction` field: LLM guidance for each section (never shown to users)
|
||||
5. **Advanced Features**:
|
||||
- Variable substitution: `{{variable_name}}` syntax for dynamic content
|
||||
- Conditional sections: `condition` field for optional content
|
||||
- Repeatable sections: `repeatable: true` for multiple instances
|
||||
- Agent permissions: `owner` and `editors` fields for access control
|
||||
6. **Clean Output**: All processing instructions are in YAML fields, ensuring clean document generation
|
||||
|
||||
## Notes for Claude Code
|
||||
|
||||
- The project uses semantic versioning with automated releases via GitHub Actions
|
||||
- All markdown is formatted with Prettier (run `npm run format`)
|
||||
- Expansion packs in `expansion-packs/` provide domain-specific capabilities
|
||||
- NEVER automatically commit or push changes unless explicitly asked by the user
|
||||
- NEVER include Claude Code attribution or co-authorship in commit messages
|
||||
@@ -75,8 +75,6 @@ This makes it easy to benefit from the latest improvements, bug fixes, and new a
|
||||
|
||||
```bash
|
||||
npx bmad-method install
|
||||
# OR explicitly use stable tag:
|
||||
npx bmad-method@stable install
|
||||
# OR if you already have BMad installed:
|
||||
git pull
|
||||
npm run install:bmad
|
||||
|
||||
@@ -4,7 +4,7 @@ bundle:
|
||||
description: Includes every core system agent.
|
||||
agents:
|
||||
- bmad-orchestrator
|
||||
- "*"
|
||||
- '*'
|
||||
workflows:
|
||||
- brownfield-fullstack.yaml
|
||||
- brownfield-service.yaml
|
||||
|
||||
@@ -131,7 +131,7 @@ workflow-guidance:
|
||||
- Understand each workflow's purpose, options, and decision points
|
||||
- Ask clarifying questions based on the workflow's structure
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -35,7 +35,7 @@ agent:
|
||||
id: dev
|
||||
title: Full Stack Developer
|
||||
icon: 💻
|
||||
whenToUse: 'Use for code implementation, debugging, refactoring, and development best practices'
|
||||
whenToUse: "Use for code implementation, debugging, refactoring, and development best practices"
|
||||
customization:
|
||||
|
||||
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.
|
||||
- exit: Say goodbye as the Developer, and then abandon inhabiting this persona
|
||||
- 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:
|
||||
- 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: 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'
|
||||
- ready-for-review: 'Code matches requirements + All validations pass + Follows standards + File List complete'
|
||||
- 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"
|
||||
- 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:
|
||||
|
||||
@@ -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`)
|
||||
- **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`)
|
||||
- **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.
|
||||
|
||||
@@ -25,10 +25,10 @@ Comprehensive guide for determining appropriate test levels (unit, integration,
|
||||
|
||||
```yaml
|
||||
unit_test:
|
||||
component: 'PriceCalculator'
|
||||
scenario: 'Calculate discount with multiple rules'
|
||||
justification: 'Complex business logic with multiple branches'
|
||||
mock_requirements: 'None - pure function'
|
||||
component: "PriceCalculator"
|
||||
scenario: "Calculate discount with multiple rules"
|
||||
justification: "Complex business logic with multiple branches"
|
||||
mock_requirements: "None - pure function"
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
@@ -52,10 +52,10 @@ unit_test:
|
||||
|
||||
```yaml
|
||||
integration_test:
|
||||
components: ['UserService', 'AuthRepository']
|
||||
scenario: 'Create user with role assignment'
|
||||
justification: 'Critical data flow between service and persistence'
|
||||
test_environment: 'In-memory database'
|
||||
components: ["UserService", "AuthRepository"]
|
||||
scenario: "Create user with role assignment"
|
||||
justification: "Critical data flow between service and persistence"
|
||||
test_environment: "In-memory database"
|
||||
```
|
||||
|
||||
### End-to-End Tests
|
||||
@@ -79,10 +79,10 @@ integration_test:
|
||||
|
||||
```yaml
|
||||
e2e_test:
|
||||
journey: 'Complete checkout process'
|
||||
scenario: 'User purchases with saved payment method'
|
||||
justification: 'Revenue-critical path requiring full validation'
|
||||
environment: 'Staging with test payment gateway'
|
||||
journey: "Complete checkout process"
|
||||
scenario: "User purchases with saved payment method"
|
||||
justification: "Revenue-critical path requiring full validation"
|
||||
environment: "Staging with test payment gateway"
|
||||
```
|
||||
|
||||
## Test Level Selection Rules
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
docOutputLocation: docs/brainstorming-session-results.md
|
||||
template: '{root}/templates/brainstorming-output-tmpl.yaml'
|
||||
template: "{root}/templates/brainstorming-output-tmpl.yaml"
|
||||
---
|
||||
|
||||
# Facilitate Brainstorming Session Task
|
||||
|
||||
@@ -6,19 +6,18 @@ Quick NFR validation focused on the core four: security, performance, reliabilit
|
||||
|
||||
```yaml
|
||||
required:
|
||||
- story_id: '{epic}.{story}' # e.g., "1.3"
|
||||
- story_path: 'docs/stories/{epic}.{story}.*.md'
|
||||
- story_id: "{epic}.{story}" # e.g., "1.3"
|
||||
- story_path: "docs/stories/{epic}.{story}.*.md"
|
||||
|
||||
optional:
|
||||
- architecture_refs: 'docs/architecture/*.md'
|
||||
- technical_preferences: 'docs/technical-preferences.md'
|
||||
- architecture_refs: "docs/architecture/*.md"
|
||||
- technical_preferences: "docs/technical-preferences.md"
|
||||
- acceptance_criteria: From story file
|
||||
```
|
||||
|
||||
## Purpose
|
||||
|
||||
Assess non-functional requirements for a story and generate:
|
||||
|
||||
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`
|
||||
|
||||
@@ -27,7 +26,6 @@ Assess non-functional requirements for a story and generate:
|
||||
### 0. Fail-safe for Missing Inputs
|
||||
|
||||
If story_path or story file can't be found:
|
||||
|
||||
- Still create assessment file with note: "Source story not found"
|
||||
- Set all selected NFRs to CONCERNS with notes: "Target unknown / evidence missing"
|
||||
- Continue with assessment to provide value
|
||||
@@ -54,7 +52,6 @@ Which NFRs should I assess? (Enter numbers or press Enter for default)
|
||||
### 2. Check for Thresholds
|
||||
|
||||
Look for NFR requirements in:
|
||||
|
||||
- Story acceptance criteria
|
||||
- `docs/architecture/*.md` files
|
||||
- `docs/technical-preferences.md`
|
||||
@@ -75,7 +72,6 @@ No security requirements found. Required auth method?
|
||||
### 3. Quick Assessment
|
||||
|
||||
For each selected NFR, check:
|
||||
|
||||
- Is there evidence it's implemented?
|
||||
- Can we validate it?
|
||||
- Are there obvious gaps?
|
||||
@@ -92,16 +88,16 @@ nfr_validation:
|
||||
_assessed: [security, performance, reliability, maintainability]
|
||||
security:
|
||||
status: CONCERNS
|
||||
notes: 'No rate limiting on auth endpoints'
|
||||
notes: "No rate limiting on auth endpoints"
|
||||
performance:
|
||||
status: PASS
|
||||
notes: 'Response times < 200ms verified'
|
||||
notes: "Response times < 200ms verified"
|
||||
reliability:
|
||||
status: PASS
|
||||
notes: 'Error handling and retries implemented'
|
||||
notes: "Error handling and retries implemented"
|
||||
maintainability:
|
||||
status: CONCERNS
|
||||
notes: 'Test coverage at 65%, target is 80%'
|
||||
notes: "Test coverage at 65%, target is 80%"
|
||||
```
|
||||
|
||||
## Deterministic Status Rules
|
||||
@@ -127,21 +123,18 @@ If `technical-preferences.md` defines custom weights, use those instead.
|
||||
|
||||
```markdown
|
||||
# NFR Assessment: {epic}.{story}
|
||||
|
||||
Date: {date}
|
||||
Reviewer: Quinn
|
||||
|
||||
<!-- Note: Source story not found (if applicable) -->
|
||||
|
||||
## Summary
|
||||
|
||||
- Security: CONCERNS - Missing rate limiting
|
||||
- Performance: PASS - Meets <200ms requirement
|
||||
- Reliability: PASS - Proper error handling
|
||||
- Maintainability: CONCERNS - Test coverage below target
|
||||
|
||||
## Critical Issues
|
||||
|
||||
1. **No rate limiting** (Security)
|
||||
- Risk: Brute force attacks possible
|
||||
- Fix: Add rate limiting middleware to auth endpoints
|
||||
@@ -151,7 +144,6 @@ Reviewer: Quinn
|
||||
- Fix: Add tests for uncovered branches
|
||||
|
||||
## Quick Wins
|
||||
|
||||
- Add rate limiting: ~2 hours
|
||||
- Increase test coverage: ~4 hours
|
||||
- Add performance monitoring: ~1 hour
|
||||
@@ -160,7 +152,6 @@ Reviewer: Quinn
|
||||
## Output 3: Story Update Line
|
||||
|
||||
**End with this line for the review task to quote:**
|
||||
|
||||
```
|
||||
NFR assessment: docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md
|
||||
```
|
||||
@@ -168,7 +159,6 @@ NFR assessment: docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md
|
||||
## Output 4: Gate Integration Line
|
||||
|
||||
**Always print at the end:**
|
||||
|
||||
```
|
||||
Gate NFR block ready → paste into docs/qa/gates/{epic}.{story}-{slug}.yml under nfr_validation
|
||||
```
|
||||
@@ -176,82 +166,66 @@ Gate NFR block ready → paste into docs/qa/gates/{epic}.{story}-{slug}.yml unde
|
||||
## Assessment Criteria
|
||||
|
||||
### Security
|
||||
|
||||
**PASS if:**
|
||||
|
||||
- Authentication implemented
|
||||
- Authorization enforced
|
||||
- Input validation present
|
||||
- No hardcoded secrets
|
||||
|
||||
**CONCERNS if:**
|
||||
|
||||
- Missing rate limiting
|
||||
- Weak encryption
|
||||
- Incomplete authorization
|
||||
|
||||
**FAIL if:**
|
||||
|
||||
- No authentication
|
||||
- Hardcoded credentials
|
||||
- SQL injection vulnerabilities
|
||||
|
||||
### Performance
|
||||
|
||||
**PASS if:**
|
||||
|
||||
- Meets response time targets
|
||||
- No obvious bottlenecks
|
||||
- Reasonable resource usage
|
||||
|
||||
**CONCERNS if:**
|
||||
|
||||
- Close to limits
|
||||
- Missing indexes
|
||||
- No caching strategy
|
||||
|
||||
**FAIL if:**
|
||||
|
||||
- Exceeds response time limits
|
||||
- Memory leaks
|
||||
- Unoptimized queries
|
||||
|
||||
### Reliability
|
||||
|
||||
**PASS if:**
|
||||
|
||||
- Error handling present
|
||||
- Graceful degradation
|
||||
- Retry logic where needed
|
||||
|
||||
**CONCERNS if:**
|
||||
|
||||
- Some error cases unhandled
|
||||
- No circuit breakers
|
||||
- Missing health checks
|
||||
|
||||
**FAIL if:**
|
||||
|
||||
- No error handling
|
||||
- Crashes on errors
|
||||
- No recovery mechanisms
|
||||
|
||||
### Maintainability
|
||||
|
||||
**PASS if:**
|
||||
|
||||
- Test coverage meets target
|
||||
- Code well-structured
|
||||
- Documentation present
|
||||
|
||||
**CONCERNS if:**
|
||||
|
||||
- Test coverage below target
|
||||
- Some code duplication
|
||||
- Missing documentation
|
||||
|
||||
**FAIL if:**
|
||||
|
||||
- No tests
|
||||
- Highly coupled code
|
||||
- No documentation
|
||||
@@ -317,7 +291,6 @@ maintainability:
|
||||
8. **Portability**: Adaptability, installability
|
||||
|
||||
Use these when assessing beyond the core four.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -331,13 +304,12 @@ performance_deep_dive:
|
||||
p99: 350ms
|
||||
database:
|
||||
slow_queries: 2
|
||||
missing_indexes: ['users.email', 'orders.user_id']
|
||||
missing_indexes: ["users.email", "orders.user_id"]
|
||||
caching:
|
||||
hit_rate: 0%
|
||||
recommendation: 'Add Redis for session data'
|
||||
recommendation: "Add Redis for session data"
|
||||
load_test:
|
||||
max_rps: 150
|
||||
breaking_point: 200 rps
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -27,11 +27,11 @@ Slug rules:
|
||||
|
||||
```yaml
|
||||
schema: 1
|
||||
story: '{epic}.{story}'
|
||||
story: "{epic}.{story}"
|
||||
gate: PASS|CONCERNS|FAIL|WAIVED
|
||||
status_reason: '1-2 sentence explanation of gate decision'
|
||||
reviewer: 'Quinn'
|
||||
updated: '{ISO-8601 timestamp}'
|
||||
status_reason: "1-2 sentence explanation of gate decision"
|
||||
reviewer: "Quinn"
|
||||
updated: "{ISO-8601 timestamp}"
|
||||
top_issues: [] # Empty array if no issues
|
||||
waiver: { active: false } # Only set active: true if WAIVED
|
||||
```
|
||||
@@ -40,20 +40,20 @@ waiver: { active: false } # Only set active: true if WAIVED
|
||||
|
||||
```yaml
|
||||
schema: 1
|
||||
story: '1.3'
|
||||
story: "1.3"
|
||||
gate: CONCERNS
|
||||
status_reason: 'Missing rate limiting on auth endpoints poses security risk.'
|
||||
reviewer: 'Quinn'
|
||||
updated: '2025-01-12T10:15:00Z'
|
||||
status_reason: "Missing rate limiting on auth endpoints poses security risk."
|
||||
reviewer: "Quinn"
|
||||
updated: "2025-01-12T10:15:00Z"
|
||||
top_issues:
|
||||
- id: 'SEC-001'
|
||||
- id: "SEC-001"
|
||||
severity: high # ONLY: low|medium|high
|
||||
finding: 'No rate limiting on login endpoint'
|
||||
suggested_action: 'Add rate limiting middleware before production'
|
||||
- id: 'TEST-001'
|
||||
finding: "No rate limiting on login endpoint"
|
||||
suggested_action: "Add rate limiting middleware before production"
|
||||
- id: "TEST-001"
|
||||
severity: medium
|
||||
finding: 'No integration tests for auth flow'
|
||||
suggested_action: 'Add integration test coverage'
|
||||
finding: "No integration tests for auth flow"
|
||||
suggested_action: "Add integration test coverage"
|
||||
waiver: { active: false }
|
||||
```
|
||||
|
||||
@@ -61,20 +61,20 @@ waiver: { active: false }
|
||||
|
||||
```yaml
|
||||
schema: 1
|
||||
story: '1.3'
|
||||
story: "1.3"
|
||||
gate: WAIVED
|
||||
status_reason: 'Known issues accepted for MVP release.'
|
||||
reviewer: 'Quinn'
|
||||
updated: '2025-01-12T10:15:00Z'
|
||||
status_reason: "Known issues accepted for MVP release."
|
||||
reviewer: "Quinn"
|
||||
updated: "2025-01-12T10:15:00Z"
|
||||
top_issues:
|
||||
- id: 'PERF-001'
|
||||
- id: "PERF-001"
|
||||
severity: low
|
||||
finding: 'Dashboard loads slowly with 1000+ items'
|
||||
suggested_action: 'Implement pagination in next sprint'
|
||||
finding: "Dashboard loads slowly with 1000+ items"
|
||||
suggested_action: "Implement pagination in next sprint"
|
||||
waiver:
|
||||
active: true
|
||||
reason: 'MVP release - performance optimization deferred'
|
||||
approved_by: 'Product Owner'
|
||||
reason: "MVP release - performance optimization deferred"
|
||||
approved_by: "Product Owner"
|
||||
```
|
||||
|
||||
## Gate Decision Criteria
|
||||
|
||||
@@ -6,10 +6,10 @@ Perform a comprehensive test architecture review with quality gate decision. Thi
|
||||
|
||||
```yaml
|
||||
required:
|
||||
- story_id: '{epic}.{story}' # e.g., "1.3"
|
||||
- story_path: '{devStoryLocation}/{epic}.{story}.*.md' # Path from core-config.yaml
|
||||
- story_title: '{title}' # If missing, derive from story file H1
|
||||
- story_slug: '{slug}' # If missing, derive from title (lowercase, hyphenated)
|
||||
- story_id: "{epic}.{story}" # e.g., "1.3"
|
||||
- story_path: "{devStoryLocation}/{epic}.{story}.*.md" # Path from core-config.yaml
|
||||
- story_title: "{title}" # If missing, derive from story file H1
|
||||
- story_slug: "{slug}" # If missing, derive from title (lowercase, hyphenated)
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
@@ -191,19 +191,19 @@ Gate file structure:
|
||||
|
||||
```yaml
|
||||
schema: 1
|
||||
story: '{epic}.{story}'
|
||||
story_title: '{story title}'
|
||||
story: "{epic}.{story}"
|
||||
story_title: "{story title}"
|
||||
gate: PASS|CONCERNS|FAIL|WAIVED
|
||||
status_reason: '1-2 sentence explanation of gate decision'
|
||||
reviewer: 'Quinn (Test Architect)'
|
||||
updated: '{ISO-8601 timestamp}'
|
||||
status_reason: "1-2 sentence explanation of gate decision"
|
||||
reviewer: "Quinn (Test Architect)"
|
||||
updated: "{ISO-8601 timestamp}"
|
||||
|
||||
top_issues: [] # Empty if no issues
|
||||
waiver: { active: false } # Set active: true only if WAIVED
|
||||
|
||||
# Extended fields (optional but recommended):
|
||||
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:
|
||||
tests_reviewed: { count }
|
||||
@@ -215,24 +215,24 @@ evidence:
|
||||
nfr_validation:
|
||||
security:
|
||||
status: PASS|CONCERNS|FAIL
|
||||
notes: 'Specific findings'
|
||||
notes: "Specific findings"
|
||||
performance:
|
||||
status: PASS|CONCERNS|FAIL
|
||||
notes: 'Specific findings'
|
||||
notes: "Specific findings"
|
||||
reliability:
|
||||
status: PASS|CONCERNS|FAIL
|
||||
notes: 'Specific findings'
|
||||
notes: "Specific findings"
|
||||
maintainability:
|
||||
status: PASS|CONCERNS|FAIL
|
||||
notes: 'Specific findings'
|
||||
notes: "Specific findings"
|
||||
|
||||
recommendations:
|
||||
immediate: # Must fix before production
|
||||
- action: 'Add rate limiting'
|
||||
refs: ['api/auth/login.ts']
|
||||
- action: "Add rate limiting"
|
||||
refs: ["api/auth/login.ts"]
|
||||
future: # Can be addressed later
|
||||
- action: 'Consider caching'
|
||||
refs: ['services/data.ts']
|
||||
- action: "Consider caching"
|
||||
refs: ["services/data.ts"]
|
||||
```
|
||||
|
||||
### Gate Decision Criteria
|
||||
|
||||
@@ -6,10 +6,10 @@ Generate a comprehensive risk assessment matrix for a story implementation using
|
||||
|
||||
```yaml
|
||||
required:
|
||||
- story_id: '{epic}.{story}' # e.g., "1.3"
|
||||
- story_path: 'docs/stories/{epic}.{story}.*.md'
|
||||
- story_title: '{title}' # If missing, derive from story file H1
|
||||
- story_slug: '{slug}' # If missing, derive from title (lowercase, hyphenated)
|
||||
- story_id: "{epic}.{story}" # e.g., "1.3"
|
||||
- story_path: "docs/stories/{epic}.{story}.*.md"
|
||||
- story_title: "{title}" # If missing, derive from story file H1
|
||||
- story_slug: "{slug}" # If missing, derive from title (lowercase, hyphenated)
|
||||
```
|
||||
|
||||
## Purpose
|
||||
@@ -79,14 +79,14 @@ For each category, identify specific risks:
|
||||
|
||||
```yaml
|
||||
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
|
||||
title: 'Insufficient input validation on user forms'
|
||||
description: 'Form inputs not properly sanitized could lead to XSS attacks'
|
||||
title: "Insufficient input validation on user forms"
|
||||
description: "Form inputs not properly sanitized could lead to XSS attacks"
|
||||
affected_components:
|
||||
- 'UserRegistrationForm'
|
||||
- 'ProfileUpdateForm'
|
||||
detection_method: 'Code review revealed missing validation'
|
||||
- "UserRegistrationForm"
|
||||
- "ProfileUpdateForm"
|
||||
detection_method: "Code review revealed missing validation"
|
||||
```
|
||||
|
||||
### 2. Risk Assessment
|
||||
@@ -133,20 +133,20 @@ For each identified risk, provide mitigation:
|
||||
|
||||
```yaml
|
||||
mitigation:
|
||||
risk_id: 'SEC-001'
|
||||
strategy: 'preventive' # preventive|detective|corrective
|
||||
risk_id: "SEC-001"
|
||||
strategy: "preventive" # preventive|detective|corrective
|
||||
actions:
|
||||
- 'Implement input validation library (e.g., validator.js)'
|
||||
- 'Add CSP headers to prevent XSS execution'
|
||||
- 'Sanitize all user inputs before storage'
|
||||
- 'Escape all outputs in templates'
|
||||
- "Implement input validation library (e.g., validator.js)"
|
||||
- "Add CSP headers to prevent XSS execution"
|
||||
- "Sanitize all user inputs before storage"
|
||||
- "Escape all outputs in templates"
|
||||
testing_requirements:
|
||||
- 'Security testing with OWASP ZAP'
|
||||
- 'Manual penetration testing of forms'
|
||||
- 'Unit tests for validation functions'
|
||||
residual_risk: 'Low - Some zero-day vulnerabilities may remain'
|
||||
owner: 'dev'
|
||||
timeline: 'Before deployment'
|
||||
- "Security testing with OWASP ZAP"
|
||||
- "Manual penetration testing of forms"
|
||||
- "Unit tests for validation functions"
|
||||
residual_risk: "Low - Some zero-day vulnerabilities may remain"
|
||||
owner: "dev"
|
||||
timeline: "Before deployment"
|
||||
```
|
||||
|
||||
## Outputs
|
||||
@@ -172,12 +172,12 @@ risk_summary:
|
||||
highest:
|
||||
id: SEC-001
|
||||
score: 9
|
||||
title: 'XSS on profile form'
|
||||
title: "XSS on profile form"
|
||||
recommendations:
|
||||
must_fix:
|
||||
- 'Add input sanitization & CSP'
|
||||
- "Add input sanitization & CSP"
|
||||
monitor:
|
||||
- 'Add security alerts for auth endpoints'
|
||||
- "Add security alerts for auth endpoints"
|
||||
```
|
||||
|
||||
### Output 2: Markdown Report
|
||||
|
||||
@@ -6,10 +6,10 @@ Create comprehensive test scenarios with appropriate test level recommendations
|
||||
|
||||
```yaml
|
||||
required:
|
||||
- story_id: '{epic}.{story}' # e.g., "1.3"
|
||||
- story_path: '{devStoryLocation}/{epic}.{story}.*.md' # Path from core-config.yaml
|
||||
- story_title: '{title}' # If missing, derive from story file H1
|
||||
- story_slug: '{slug}' # If missing, derive from title (lowercase, hyphenated)
|
||||
- story_id: "{epic}.{story}" # e.g., "1.3"
|
||||
- story_path: "{devStoryLocation}/{epic}.{story}.*.md" # Path from core-config.yaml
|
||||
- story_title: "{title}" # If missing, derive from story file H1
|
||||
- story_slug: "{slug}" # If missing, derive from title (lowercase, hyphenated)
|
||||
```
|
||||
|
||||
## Purpose
|
||||
@@ -62,13 +62,13 @@ For each identified test need, create:
|
||||
|
||||
```yaml
|
||||
test_scenario:
|
||||
id: '{epic}.{story}-{LEVEL}-{SEQ}'
|
||||
requirement: 'AC reference'
|
||||
id: "{epic}.{story}-{LEVEL}-{SEQ}"
|
||||
requirement: "AC reference"
|
||||
priority: P0|P1|P2|P3
|
||||
level: unit|integration|e2e
|
||||
description: 'What is being tested'
|
||||
justification: 'Why this level was chosen'
|
||||
mitigates_risks: ['RISK-001'] # If risk profile exists
|
||||
description: "What is being tested"
|
||||
justification: "Why this level was chosen"
|
||||
mitigates_risks: ["RISK-001"] # If risk profile exists
|
||||
```
|
||||
|
||||
### 5. Validate Coverage
|
||||
|
||||
@@ -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):
|
||||
|
||||
```yaml
|
||||
requirement: 'AC1: User can login with valid credentials'
|
||||
requirement: "AC1: User can login with valid credentials"
|
||||
test_mappings:
|
||||
- test_file: 'auth/login.test.ts'
|
||||
test_case: 'should successfully login with valid email and password'
|
||||
- test_file: "auth/login.test.ts"
|
||||
test_case: "should successfully login with valid email and password"
|
||||
# Given-When-Then describes WHAT the test validates, not HOW it's coded
|
||||
given: 'A registered user with valid credentials'
|
||||
when: 'They submit the login form'
|
||||
then: 'They are redirected to dashboard and session is created'
|
||||
given: "A registered user with valid credentials"
|
||||
when: "They submit the login form"
|
||||
then: "They are redirected to dashboard and session is created"
|
||||
coverage: full
|
||||
|
||||
- test_file: 'e2e/auth-flow.test.ts'
|
||||
test_case: 'complete login flow'
|
||||
given: 'User on login page'
|
||||
when: 'Entering valid credentials and submitting'
|
||||
then: 'Dashboard loads with user data'
|
||||
- test_file: "e2e/auth-flow.test.ts"
|
||||
test_case: "complete login flow"
|
||||
given: "User on login page"
|
||||
when: "Entering valid credentials and submitting"
|
||||
then: "Dashboard loads with user data"
|
||||
coverage: integration
|
||||
```
|
||||
|
||||
@@ -67,19 +67,19 @@ Document any gaps found:
|
||||
|
||||
```yaml
|
||||
coverage_gaps:
|
||||
- requirement: 'AC3: Password reset email sent within 60 seconds'
|
||||
gap: 'No test for email delivery timing'
|
||||
- requirement: "AC3: Password reset email sent within 60 seconds"
|
||||
gap: "No test for email delivery timing"
|
||||
severity: medium
|
||||
suggested_test:
|
||||
type: integration
|
||||
description: 'Test email service SLA compliance'
|
||||
description: "Test email service SLA compliance"
|
||||
|
||||
- requirement: 'AC5: Support 1000 concurrent users'
|
||||
gap: 'No load testing implemented'
|
||||
- requirement: "AC5: Support 1000 concurrent users"
|
||||
gap: "No load testing implemented"
|
||||
severity: high
|
||||
suggested_test:
|
||||
type: performance
|
||||
description: 'Load test with 1000 concurrent connections'
|
||||
description: "Load test with 1000 concurrent connections"
|
||||
```
|
||||
|
||||
## Outputs
|
||||
@@ -95,11 +95,11 @@ trace:
|
||||
full: Y
|
||||
partial: Z
|
||||
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:
|
||||
- ac: 'AC3'
|
||||
reason: 'No test found for password reset timing'
|
||||
notes: 'See docs/qa/assessments/{epic}.{story}-trace-{YYYYMMDD}.md'
|
||||
- ac: "AC3"
|
||||
reason: "No test found for password reset timing"
|
||||
notes: "See docs/qa/assessments/{epic}.{story}-trace-{YYYYMMDD}.md"
|
||||
```
|
||||
|
||||
### Output 2: Traceability Report
|
||||
|
||||
@@ -141,14 +141,7 @@ sections:
|
||||
title: Feature Comparison Matrix
|
||||
instruction: Create a detailed comparison table of key features across competitors
|
||||
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:
|
||||
- category: "Core Functionality"
|
||||
items:
|
||||
@@ -160,13 +153,7 @@ sections:
|
||||
- ["Onboarding Time", "{{time}}", "{{time}}", "{{time}}", "{{time}}"]
|
||||
- category: "Integration & Ecosystem"
|
||||
items:
|
||||
- [
|
||||
"API Availability",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
]
|
||||
- ["API Availability", "{{availability}}", "{{availability}}", "{{availability}}", "{{availability}}"]
|
||||
- ["Third-party Integrations", "{{number}}", "{{number}}", "{{number}}", "{{number}}"]
|
||||
- category: "Pricing & Plans"
|
||||
items:
|
||||
|
||||
@@ -75,24 +75,12 @@ sections:
|
||||
rows:
|
||||
- ["Framework", "{{framework}}", "{{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}}"]
|
||||
- ["Build Tool", "{{build_tool}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Styling", "{{styling_solution}}", "{{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}}"]
|
||||
- ["Animation", "{{animation_lib}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Dev Tools", "{{dev_tools}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
|
||||
@@ -156,29 +156,11 @@ sections:
|
||||
columns: [Category, Technology, Version, Purpose, Rationale]
|
||||
rows:
|
||||
- ["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}}"]
|
||||
- ["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}}"]
|
||||
- ["Database", "{{database}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Cache", "{{cache}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
@@ -506,60 +488,60 @@ sections:
|
||||
type: code
|
||||
language: plaintext
|
||||
examples:
|
||||
- |
|
||||
{{project-name}}/
|
||||
├── .github/ # CI/CD workflows
|
||||
│ └── workflows/
|
||||
│ ├── ci.yaml
|
||||
│ └── deploy.yaml
|
||||
├── apps/ # Application packages
|
||||
│ ├── web/ # Frontend application
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # UI components
|
||||
│ │ │ ├── pages/ # Page components/routes
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API client services
|
||||
│ │ │ ├── stores/ # State management
|
||||
│ │ │ ├── styles/ # Global styles/themes
|
||||
│ │ │ └── utils/ # Frontend utilities
|
||||
│ │ ├── public/ # Static assets
|
||||
│ │ ├── tests/ # Frontend tests
|
||||
│ │ └── package.json
|
||||
│ └── api/ # Backend application
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API routes/controllers
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ ├── middleware/ # Express/API middleware
|
||||
│ │ ├── utils/ # Backend utilities
|
||||
│ │ └── {{serverless_or_server_entry}}
|
||||
│ ├── tests/ # Backend tests
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
│ ├── shared/ # Shared types/utilities
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── types/ # TypeScript interfaces
|
||||
│ │ │ ├── constants/ # Shared constants
|
||||
│ │ │ └── utils/ # Shared utilities
|
||||
│ │ └── package.json
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── config/ # Shared configuration
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── jest/
|
||||
├── infrastructure/ # IaC definitions
|
||||
│ └── {{iac_structure}}
|
||||
├── scripts/ # Build/deploy scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── prd.md
|
||||
│ ├── front-end-spec.md
|
||||
│ └── fullstack-architecture.md
|
||||
├── .env.example # Environment template
|
||||
├── package.json # Root package.json
|
||||
├── {{monorepo_config}} # Monorepo configuration
|
||||
└── README.md
|
||||
- |
|
||||
{{project-name}}/
|
||||
├── .github/ # CI/CD workflows
|
||||
│ └── workflows/
|
||||
│ ├── ci.yaml
|
||||
│ └── deploy.yaml
|
||||
├── apps/ # Application packages
|
||||
│ ├── web/ # Frontend application
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # UI components
|
||||
│ │ │ ├── pages/ # Page components/routes
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API client services
|
||||
│ │ │ ├── stores/ # State management
|
||||
│ │ │ ├── styles/ # Global styles/themes
|
||||
│ │ │ └── utils/ # Frontend utilities
|
||||
│ │ ├── public/ # Static assets
|
||||
│ │ ├── tests/ # Frontend tests
|
||||
│ │ └── package.json
|
||||
│ └── api/ # Backend application
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API routes/controllers
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ ├── middleware/ # Express/API middleware
|
||||
│ │ ├── utils/ # Backend utilities
|
||||
│ │ └── {{serverless_or_server_entry}}
|
||||
│ ├── tests/ # Backend tests
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
│ ├── shared/ # Shared types/utilities
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── types/ # TypeScript interfaces
|
||||
│ │ │ ├── constants/ # Shared constants
|
||||
│ │ │ └── utils/ # Shared utilities
|
||||
│ │ └── package.json
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── config/ # Shared configuration
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── jest/
|
||||
├── infrastructure/ # IaC definitions
|
||||
│ └── {{iac_structure}}
|
||||
├── scripts/ # Build/deploy scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── prd.md
|
||||
│ ├── front-end-spec.md
|
||||
│ └── fullstack-architecture.md
|
||||
├── .env.example # Environment template
|
||||
├── package.json # Root package.json
|
||||
├── {{monorepo_config}} # Monorepo configuration
|
||||
└── README.md
|
||||
|
||||
- id: development-workflow
|
||||
title: Development Workflow
|
||||
@@ -689,10 +671,10 @@ sections:
|
||||
type: code
|
||||
language: text
|
||||
template: |
|
||||
E2E Tests
|
||||
/ \
|
||||
Integration Tests
|
||||
/ \
|
||||
E2E Tests
|
||||
/ \
|
||||
Integration Tests
|
||||
/ \
|
||||
Frontend Unit Backend Unit
|
||||
- id: test-organization
|
||||
title: Test Organization
|
||||
|
||||
@@ -11,8 +11,8 @@ template:
|
||||
schema: 1
|
||||
story: "{{epic_num}}.{{story_num}}"
|
||||
story_title: "{{story_title}}"
|
||||
gate: "{{gate_status}}" # PASS|CONCERNS|FAIL|WAIVED
|
||||
status_reason: "{{status_reason}}" # 1-2 sentence summary of why this gate decision
|
||||
gate: "{{gate_status}}" # PASS|CONCERNS|FAIL|WAIVED
|
||||
status_reason: "{{status_reason}}" # 1-2 sentence summary of why this gate decision
|
||||
reviewer: "Quinn (Test Architect)"
|
||||
updated: "{{iso_timestamp}}"
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ template:
|
||||
output:
|
||||
format: markdown
|
||||
filename: default-path/to/{{filename}}.md
|
||||
title: '{{variable}} Document Title'
|
||||
title: "{{variable}} Document Title"
|
||||
|
||||
workflow:
|
||||
mode: interactive
|
||||
@@ -108,8 +108,8 @@ sections:
|
||||
Use `{{variable_name}}` in titles, templates, and content:
|
||||
|
||||
```yaml
|
||||
title: 'Epic {{epic_number}} {{epic_title}}'
|
||||
template: 'As a {{user_type}}, I want {{action}}, so that {{benefit}}.'
|
||||
title: "Epic {{epic_number}} {{epic_title}}"
|
||||
template: "As a {{user_type}}, I want {{action}}, so that {{benefit}}."
|
||||
```
|
||||
|
||||
### Conditional Sections
|
||||
@@ -212,7 +212,7 @@ choices:
|
||||
- id: criteria
|
||||
title: Acceptance Criteria
|
||||
type: numbered-list
|
||||
item_template: '{{criterion_number}}: {{criteria}}'
|
||||
item_template: "{{criterion_number}}: {{criteria}}"
|
||||
repeatable: true
|
||||
```
|
||||
|
||||
@@ -220,7 +220,7 @@ choices:
|
||||
|
||||
````yaml
|
||||
examples:
|
||||
- 'FR6: The system must authenticate users within 2 seconds'
|
||||
- "FR6: The system must authenticate users within 2 seconds"
|
||||
- |
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
|
||||
21
dist/agents/analyst.txt
vendored
21
dist/agents/analyst.txt
vendored
@@ -106,7 +106,7 @@ dependencies:
|
||||
==================== START: .bmad-core/tasks/facilitate-brainstorming-session.md ====================
|
||||
---
|
||||
docOutputLocation: docs/brainstorming-session-results.md
|
||||
template: '.bmad-core/templates/brainstorming-output-tmpl.yaml'
|
||||
template: ".bmad-core/templates/brainstorming-output-tmpl.yaml"
|
||||
---
|
||||
|
||||
# Facilitate Brainstorming Session Task
|
||||
@@ -1716,14 +1716,7 @@ sections:
|
||||
title: Feature Comparison Matrix
|
||||
instruction: Create a detailed comparison table of key features across competitors
|
||||
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:
|
||||
- category: "Core Functionality"
|
||||
items:
|
||||
@@ -1735,13 +1728,7 @@ sections:
|
||||
- ["Onboarding Time", "{{time}}", "{{time}}", "{{time}}", "{{time}}"]
|
||||
- category: "Integration & Ecosystem"
|
||||
items:
|
||||
- [
|
||||
"API Availability",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
]
|
||||
- ["API Availability", "{{availability}}", "{{availability}}", "{{availability}}", "{{availability}}"]
|
||||
- ["Third-party Integrations", "{{number}}", "{{number}}", "{{number}}", "{{number}}"]
|
||||
- category: "Pricing & Plans"
|
||||
items:
|
||||
@@ -2341,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`)
|
||||
- **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`)
|
||||
- **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.
|
||||
|
||||
156
dist/agents/architect.txt
vendored
156
dist/agents/architect.txt
vendored
@@ -1656,24 +1656,12 @@ sections:
|
||||
rows:
|
||||
- ["Framework", "{{framework}}", "{{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}}"]
|
||||
- ["Build Tool", "{{build_tool}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Styling", "{{styling_solution}}", "{{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}}"]
|
||||
- ["Animation", "{{animation_lib}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Dev Tools", "{{dev_tools}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
@@ -1958,29 +1946,11 @@ sections:
|
||||
columns: [Category, Technology, Version, Purpose, Rationale]
|
||||
rows:
|
||||
- ["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}}"]
|
||||
- ["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}}"]
|
||||
- ["Database", "{{database}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Cache", "{{cache}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
@@ -2308,60 +2278,60 @@ sections:
|
||||
type: code
|
||||
language: plaintext
|
||||
examples:
|
||||
- |
|
||||
{{project-name}}/
|
||||
├── .github/ # CI/CD workflows
|
||||
│ └── workflows/
|
||||
│ ├── ci.yaml
|
||||
│ └── deploy.yaml
|
||||
├── apps/ # Application packages
|
||||
│ ├── web/ # Frontend application
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # UI components
|
||||
│ │ │ ├── pages/ # Page components/routes
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API client services
|
||||
│ │ │ ├── stores/ # State management
|
||||
│ │ │ ├── styles/ # Global styles/themes
|
||||
│ │ │ └── utils/ # Frontend utilities
|
||||
│ │ ├── public/ # Static assets
|
||||
│ │ ├── tests/ # Frontend tests
|
||||
│ │ └── package.json
|
||||
│ └── api/ # Backend application
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API routes/controllers
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ ├── middleware/ # Express/API middleware
|
||||
│ │ ├── utils/ # Backend utilities
|
||||
│ │ └── {{serverless_or_server_entry}}
|
||||
│ ├── tests/ # Backend tests
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
│ ├── shared/ # Shared types/utilities
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── types/ # TypeScript interfaces
|
||||
│ │ │ ├── constants/ # Shared constants
|
||||
│ │ │ └── utils/ # Shared utilities
|
||||
│ │ └── package.json
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── config/ # Shared configuration
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── jest/
|
||||
├── infrastructure/ # IaC definitions
|
||||
│ └── {{iac_structure}}
|
||||
├── scripts/ # Build/deploy scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── prd.md
|
||||
│ ├── front-end-spec.md
|
||||
│ └── fullstack-architecture.md
|
||||
├── .env.example # Environment template
|
||||
├── package.json # Root package.json
|
||||
├── {{monorepo_config}} # Monorepo configuration
|
||||
└── README.md
|
||||
- |
|
||||
{{project-name}}/
|
||||
├── .github/ # CI/CD workflows
|
||||
│ └── workflows/
|
||||
│ ├── ci.yaml
|
||||
│ └── deploy.yaml
|
||||
├── apps/ # Application packages
|
||||
│ ├── web/ # Frontend application
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # UI components
|
||||
│ │ │ ├── pages/ # Page components/routes
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API client services
|
||||
│ │ │ ├── stores/ # State management
|
||||
│ │ │ ├── styles/ # Global styles/themes
|
||||
│ │ │ └── utils/ # Frontend utilities
|
||||
│ │ ├── public/ # Static assets
|
||||
│ │ ├── tests/ # Frontend tests
|
||||
│ │ └── package.json
|
||||
│ └── api/ # Backend application
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API routes/controllers
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ ├── middleware/ # Express/API middleware
|
||||
│ │ ├── utils/ # Backend utilities
|
||||
│ │ └── {{serverless_or_server_entry}}
|
||||
│ ├── tests/ # Backend tests
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
│ ├── shared/ # Shared types/utilities
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── types/ # TypeScript interfaces
|
||||
│ │ │ ├── constants/ # Shared constants
|
||||
│ │ │ └── utils/ # Shared utilities
|
||||
│ │ └── package.json
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── config/ # Shared configuration
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── jest/
|
||||
├── infrastructure/ # IaC definitions
|
||||
│ └── {{iac_structure}}
|
||||
├── scripts/ # Build/deploy scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── prd.md
|
||||
│ ├── front-end-spec.md
|
||||
│ └── fullstack-architecture.md
|
||||
├── .env.example # Environment template
|
||||
├── package.json # Root package.json
|
||||
├── {{monorepo_config}} # Monorepo configuration
|
||||
└── README.md
|
||||
|
||||
- id: development-workflow
|
||||
title: Development Workflow
|
||||
@@ -2491,10 +2461,10 @@ sections:
|
||||
type: code
|
||||
language: text
|
||||
template: |
|
||||
E2E Tests
|
||||
/ \
|
||||
Integration Tests
|
||||
/ \
|
||||
E2E Tests
|
||||
/ \
|
||||
Integration Tests
|
||||
/ \
|
||||
Frontend Unit Backend Unit
|
||||
- id: test-organization
|
||||
title: Test Organization
|
||||
|
||||
177
dist/agents/bmad-master.txt
vendored
177
dist/agents/bmad-master.txt
vendored
@@ -248,7 +248,7 @@ Choose a number (0-8) or 9 to proceed:
|
||||
==================== START: .bmad-core/tasks/facilitate-brainstorming-session.md ====================
|
||||
---
|
||||
docOutputLocation: docs/brainstorming-session-results.md
|
||||
template: '.bmad-core/templates/brainstorming-output-tmpl.yaml'
|
||||
template: ".bmad-core/templates/brainstorming-output-tmpl.yaml"
|
||||
---
|
||||
|
||||
# Facilitate Brainstorming Session Task
|
||||
@@ -3682,14 +3682,7 @@ sections:
|
||||
title: Feature Comparison Matrix
|
||||
instruction: Create a detailed comparison table of key features across competitors
|
||||
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:
|
||||
- category: "Core Functionality"
|
||||
items:
|
||||
@@ -3701,13 +3694,7 @@ sections:
|
||||
- ["Onboarding Time", "{{time}}", "{{time}}", "{{time}}", "{{time}}"]
|
||||
- category: "Integration & Ecosystem"
|
||||
items:
|
||||
- [
|
||||
"API Availability",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
]
|
||||
- ["API Availability", "{{availability}}", "{{availability}}", "{{availability}}", "{{availability}}"]
|
||||
- ["Third-party Integrations", "{{number}}", "{{number}}", "{{number}}", "{{number}}"]
|
||||
- category: "Pricing & Plans"
|
||||
items:
|
||||
@@ -3925,24 +3912,12 @@ sections:
|
||||
rows:
|
||||
- ["Framework", "{{framework}}", "{{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}}"]
|
||||
- ["Build Tool", "{{build_tool}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Styling", "{{styling_solution}}", "{{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}}"]
|
||||
- ["Animation", "{{animation_lib}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Dev Tools", "{{dev_tools}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
@@ -4579,29 +4554,11 @@ sections:
|
||||
columns: [Category, Technology, Version, Purpose, Rationale]
|
||||
rows:
|
||||
- ["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}}"]
|
||||
- ["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}}"]
|
||||
- ["Database", "{{database}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Cache", "{{cache}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
@@ -4929,60 +4886,60 @@ sections:
|
||||
type: code
|
||||
language: plaintext
|
||||
examples:
|
||||
- |
|
||||
{{project-name}}/
|
||||
├── .github/ # CI/CD workflows
|
||||
│ └── workflows/
|
||||
│ ├── ci.yaml
|
||||
│ └── deploy.yaml
|
||||
├── apps/ # Application packages
|
||||
│ ├── web/ # Frontend application
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # UI components
|
||||
│ │ │ ├── pages/ # Page components/routes
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API client services
|
||||
│ │ │ ├── stores/ # State management
|
||||
│ │ │ ├── styles/ # Global styles/themes
|
||||
│ │ │ └── utils/ # Frontend utilities
|
||||
│ │ ├── public/ # Static assets
|
||||
│ │ ├── tests/ # Frontend tests
|
||||
│ │ └── package.json
|
||||
│ └── api/ # Backend application
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API routes/controllers
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ ├── middleware/ # Express/API middleware
|
||||
│ │ ├── utils/ # Backend utilities
|
||||
│ │ └── {{serverless_or_server_entry}}
|
||||
│ ├── tests/ # Backend tests
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
│ ├── shared/ # Shared types/utilities
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── types/ # TypeScript interfaces
|
||||
│ │ │ ├── constants/ # Shared constants
|
||||
│ │ │ └── utils/ # Shared utilities
|
||||
│ │ └── package.json
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── config/ # Shared configuration
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── jest/
|
||||
├── infrastructure/ # IaC definitions
|
||||
│ └── {{iac_structure}}
|
||||
├── scripts/ # Build/deploy scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── prd.md
|
||||
│ ├── front-end-spec.md
|
||||
│ └── fullstack-architecture.md
|
||||
├── .env.example # Environment template
|
||||
├── package.json # Root package.json
|
||||
├── {{monorepo_config}} # Monorepo configuration
|
||||
└── README.md
|
||||
- |
|
||||
{{project-name}}/
|
||||
├── .github/ # CI/CD workflows
|
||||
│ └── workflows/
|
||||
│ ├── ci.yaml
|
||||
│ └── deploy.yaml
|
||||
├── apps/ # Application packages
|
||||
│ ├── web/ # Frontend application
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # UI components
|
||||
│ │ │ ├── pages/ # Page components/routes
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API client services
|
||||
│ │ │ ├── stores/ # State management
|
||||
│ │ │ ├── styles/ # Global styles/themes
|
||||
│ │ │ └── utils/ # Frontend utilities
|
||||
│ │ ├── public/ # Static assets
|
||||
│ │ ├── tests/ # Frontend tests
|
||||
│ │ └── package.json
|
||||
│ └── api/ # Backend application
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API routes/controllers
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ ├── middleware/ # Express/API middleware
|
||||
│ │ ├── utils/ # Backend utilities
|
||||
│ │ └── {{serverless_or_server_entry}}
|
||||
│ ├── tests/ # Backend tests
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
│ ├── shared/ # Shared types/utilities
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── types/ # TypeScript interfaces
|
||||
│ │ │ ├── constants/ # Shared constants
|
||||
│ │ │ └── utils/ # Shared utilities
|
||||
│ │ └── package.json
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── config/ # Shared configuration
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── jest/
|
||||
├── infrastructure/ # IaC definitions
|
||||
│ └── {{iac_structure}}
|
||||
├── scripts/ # Build/deploy scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── prd.md
|
||||
│ ├── front-end-spec.md
|
||||
│ └── fullstack-architecture.md
|
||||
├── .env.example # Environment template
|
||||
├── package.json # Root package.json
|
||||
├── {{monorepo_config}} # Monorepo configuration
|
||||
└── README.md
|
||||
|
||||
- id: development-workflow
|
||||
title: Development Workflow
|
||||
@@ -5112,10 +5069,10 @@ sections:
|
||||
type: code
|
||||
language: text
|
||||
template: |
|
||||
E2E Tests
|
||||
/ \
|
||||
Integration Tests
|
||||
/ \
|
||||
E2E Tests
|
||||
/ \
|
||||
Integration Tests
|
||||
/ \
|
||||
Frontend Unit Backend Unit
|
||||
- id: test-organization
|
||||
title: Test Organization
|
||||
@@ -8058,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`)
|
||||
- **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`)
|
||||
- **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.
|
||||
|
||||
2
dist/agents/bmad-orchestrator.txt
vendored
2
dist/agents/bmad-orchestrator.txt
vendored
@@ -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`)
|
||||
- **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`)
|
||||
- **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.
|
||||
|
||||
859
dist/agents/qa.txt
vendored
859
dist/agents/qa.txt
vendored
File diff suppressed because it is too large
Load Diff
@@ -1045,7 +1045,7 @@ interface GameState {
|
||||
interface GameSettings {
|
||||
musicVolume: number;
|
||||
sfxVolume: number;
|
||||
difficulty: 'easy' | 'normal' | 'hard';
|
||||
difficulty: "easy" | "normal" | "hard";
|
||||
controls: ControlScheme;
|
||||
}
|
||||
```
|
||||
@@ -1086,12 +1086,12 @@ class GameScene extends Phaser.Scene {
|
||||
private inputManager!: InputManager;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
super({ key: "GameScene" });
|
||||
}
|
||||
|
||||
preload(): void {
|
||||
// Load only scene-specific assets
|
||||
this.load.image('player', 'assets/player.png');
|
||||
this.load.image("player", "assets/player.png");
|
||||
}
|
||||
|
||||
create(data: SceneData): void {
|
||||
@@ -1116,7 +1116,7 @@ class GameScene extends Phaser.Scene {
|
||||
this.inputManager.destroy();
|
||||
|
||||
// Remove event listeners
|
||||
this.events.off('*');
|
||||
this.events.off("*");
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1125,13 +1125,13 @@ class GameScene extends Phaser.Scene {
|
||||
|
||||
```typescript
|
||||
// Proper scene transitions with data
|
||||
this.scene.start('NextScene', {
|
||||
this.scene.start("NextScene", {
|
||||
playerScore: this.playerScore,
|
||||
currentLevel: this.currentLevel + 1,
|
||||
});
|
||||
|
||||
// Scene overlays for UI
|
||||
this.scene.launch('PauseMenuScene');
|
||||
this.scene.launch("PauseMenuScene");
|
||||
this.scene.pause();
|
||||
```
|
||||
|
||||
@@ -1175,7 +1175,7 @@ class Player extends GameEntity {
|
||||
private health!: HealthComponent;
|
||||
|
||||
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.health = this.addComponent(new HealthComponent(this, 100));
|
||||
@@ -1195,7 +1195,7 @@ class GameManager {
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
if (GameManager.instance) {
|
||||
throw new Error('GameManager already exists!');
|
||||
throw new Error("GameManager already exists!");
|
||||
}
|
||||
|
||||
this.scene = scene;
|
||||
@@ -1205,7 +1205,7 @@ class GameManager {
|
||||
|
||||
static getInstance(): GameManager {
|
||||
if (!GameManager.instance) {
|
||||
throw new Error('GameManager not initialized!');
|
||||
throw new Error("GameManager not initialized!");
|
||||
}
|
||||
return GameManager.instance;
|
||||
}
|
||||
@@ -1252,7 +1252,7 @@ class BulletPool {
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -1352,12 +1352,14 @@ class InputManager {
|
||||
}
|
||||
|
||||
private setupKeyboard(): void {
|
||||
this.keys = this.scene.input.keyboard.addKeys('W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT');
|
||||
this.keys = this.scene.input.keyboard.addKeys(
|
||||
"W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT",
|
||||
);
|
||||
}
|
||||
|
||||
private setupTouch(): void {
|
||||
this.scene.input.on('pointerdown', this.handlePointerDown, this);
|
||||
this.scene.input.on('pointerup', this.handlePointerUp, this);
|
||||
this.scene.input.on("pointerdown", this.handlePointerDown, this);
|
||||
this.scene.input.on("pointerup", this.handlePointerUp, this);
|
||||
}
|
||||
|
||||
update(): void {
|
||||
@@ -1384,9 +1386,9 @@ class InputManager {
|
||||
class AssetManager {
|
||||
loadAssets(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.scene.load.on('filecomplete', this.handleFileComplete, this);
|
||||
this.scene.load.on('loaderror', this.handleLoadError, this);
|
||||
this.scene.load.on('complete', () => resolve());
|
||||
this.scene.load.on("filecomplete", this.handleFileComplete, this);
|
||||
this.scene.load.on("loaderror", this.handleLoadError, this);
|
||||
this.scene.load.on("complete", () => resolve());
|
||||
|
||||
this.scene.load.start();
|
||||
});
|
||||
@@ -1402,8 +1404,8 @@ class AssetManager {
|
||||
private loadFallbackAsset(key: string): void {
|
||||
// Load placeholder or default assets
|
||||
switch (key) {
|
||||
case 'player':
|
||||
this.scene.load.image('player', 'assets/defaults/default-player.png');
|
||||
case "player":
|
||||
this.scene.load.image("player", "assets/defaults/default-player.png");
|
||||
break;
|
||||
default:
|
||||
console.warn(`No fallback for asset: ${key}`);
|
||||
@@ -1430,11 +1432,11 @@ class GameSystem {
|
||||
|
||||
private attemptRecovery(context: string): void {
|
||||
switch (context) {
|
||||
case 'update':
|
||||
case "update":
|
||||
// Reset system state
|
||||
this.reset();
|
||||
break;
|
||||
case 'render':
|
||||
case "render":
|
||||
// Disable visual effects
|
||||
this.disableEffects();
|
||||
break;
|
||||
@@ -1454,7 +1456,7 @@ class GameSystem {
|
||||
|
||||
```typescript
|
||||
// Example test for game mechanics
|
||||
describe('HealthComponent', () => {
|
||||
describe("HealthComponent", () => {
|
||||
let healthComponent: HealthComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1462,18 +1464,18 @@ describe('HealthComponent', () => {
|
||||
healthComponent = new HealthComponent(mockEntity, 100);
|
||||
});
|
||||
|
||||
test('should initialize with correct health', () => {
|
||||
test("should initialize with correct health", () => {
|
||||
expect(healthComponent.currentHealth).toBe(100);
|
||||
expect(healthComponent.maxHealth).toBe(100);
|
||||
});
|
||||
|
||||
test('should handle damage correctly', () => {
|
||||
test("should handle damage correctly", () => {
|
||||
healthComponent.takeDamage(25);
|
||||
expect(healthComponent.currentHealth).toBe(75);
|
||||
expect(healthComponent.isAlive()).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle death correctly', () => {
|
||||
test("should handle death correctly", () => {
|
||||
healthComponent.takeDamage(150);
|
||||
expect(healthComponent.currentHealth).toBe(0);
|
||||
expect(healthComponent.isAlive()).toBe(false);
|
||||
@@ -1486,7 +1488,7 @@ describe('HealthComponent', () => {
|
||||
**Scene Testing:**
|
||||
|
||||
```typescript
|
||||
describe('GameScene Integration', () => {
|
||||
describe("GameScene Integration", () => {
|
||||
let scene: GameScene;
|
||||
let mockGame: Phaser.Game;
|
||||
|
||||
@@ -1496,7 +1498,7 @@ describe('GameScene Integration', () => {
|
||||
scene = new GameScene();
|
||||
});
|
||||
|
||||
test('should initialize all systems', () => {
|
||||
test("should initialize all systems", () => {
|
||||
scene.create({});
|
||||
|
||||
expect(scene.gameManager).toBeDefined();
|
||||
|
||||
@@ -420,7 +420,7 @@ dependencies:
|
||||
==================== START: .bmad-2d-phaser-game-dev/tasks/facilitate-brainstorming-session.md ====================
|
||||
---
|
||||
docOutputLocation: docs/brainstorming-session-results.md
|
||||
template: '.bmad-2d-phaser-game-dev/templates/brainstorming-output-tmpl.yaml'
|
||||
template: ".bmad-2d-phaser-game-dev/templates/brainstorming-output-tmpl.yaml"
|
||||
---
|
||||
|
||||
# Facilitate Brainstorming Session Task
|
||||
@@ -2023,14 +2023,7 @@ sections:
|
||||
title: Feature Comparison Matrix
|
||||
instruction: Create a detailed comparison table of key features across competitors
|
||||
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:
|
||||
- category: "Core Functionality"
|
||||
items:
|
||||
@@ -2042,13 +2035,7 @@ sections:
|
||||
- ["Onboarding Time", "{{time}}", "{{time}}", "{{time}}", "{{time}}"]
|
||||
- category: "Integration & Ecosystem"
|
||||
items:
|
||||
- [
|
||||
"API Availability",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
]
|
||||
- ["API Availability", "{{availability}}", "{{availability}}", "{{availability}}", "{{availability}}"]
|
||||
- ["Third-party Integrations", "{{number}}", "{{number}}", "{{number}}", "{{number}}"]
|
||||
- category: "Pricing & Plans"
|
||||
items:
|
||||
@@ -5579,7 +5566,7 @@ interface GameState {
|
||||
interface GameSettings {
|
||||
musicVolume: number;
|
||||
sfxVolume: number;
|
||||
difficulty: 'easy' | 'normal' | 'hard';
|
||||
difficulty: "easy" | "normal" | "hard";
|
||||
controls: ControlScheme;
|
||||
}
|
||||
```
|
||||
@@ -5620,12 +5607,12 @@ class GameScene extends Phaser.Scene {
|
||||
private inputManager!: InputManager;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
super({ key: "GameScene" });
|
||||
}
|
||||
|
||||
preload(): void {
|
||||
// Load only scene-specific assets
|
||||
this.load.image('player', 'assets/player.png');
|
||||
this.load.image("player", "assets/player.png");
|
||||
}
|
||||
|
||||
create(data: SceneData): void {
|
||||
@@ -5650,7 +5637,7 @@ class GameScene extends Phaser.Scene {
|
||||
this.inputManager.destroy();
|
||||
|
||||
// Remove event listeners
|
||||
this.events.off('*');
|
||||
this.events.off("*");
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -5659,13 +5646,13 @@ class GameScene extends Phaser.Scene {
|
||||
|
||||
```typescript
|
||||
// Proper scene transitions with data
|
||||
this.scene.start('NextScene', {
|
||||
this.scene.start("NextScene", {
|
||||
playerScore: this.playerScore,
|
||||
currentLevel: this.currentLevel + 1,
|
||||
});
|
||||
|
||||
// Scene overlays for UI
|
||||
this.scene.launch('PauseMenuScene');
|
||||
this.scene.launch("PauseMenuScene");
|
||||
this.scene.pause();
|
||||
```
|
||||
|
||||
@@ -5709,7 +5696,7 @@ class Player extends GameEntity {
|
||||
private health!: HealthComponent;
|
||||
|
||||
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.health = this.addComponent(new HealthComponent(this, 100));
|
||||
@@ -5729,7 +5716,7 @@ class GameManager {
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
if (GameManager.instance) {
|
||||
throw new Error('GameManager already exists!');
|
||||
throw new Error("GameManager already exists!");
|
||||
}
|
||||
|
||||
this.scene = scene;
|
||||
@@ -5739,7 +5726,7 @@ class GameManager {
|
||||
|
||||
static getInstance(): GameManager {
|
||||
if (!GameManager.instance) {
|
||||
throw new Error('GameManager not initialized!');
|
||||
throw new Error("GameManager not initialized!");
|
||||
}
|
||||
return GameManager.instance;
|
||||
}
|
||||
@@ -5786,7 +5773,7 @@ class BulletPool {
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -5886,12 +5873,14 @@ class InputManager {
|
||||
}
|
||||
|
||||
private setupKeyboard(): void {
|
||||
this.keys = this.scene.input.keyboard.addKeys('W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT');
|
||||
this.keys = this.scene.input.keyboard.addKeys(
|
||||
"W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT",
|
||||
);
|
||||
}
|
||||
|
||||
private setupTouch(): void {
|
||||
this.scene.input.on('pointerdown', this.handlePointerDown, this);
|
||||
this.scene.input.on('pointerup', this.handlePointerUp, this);
|
||||
this.scene.input.on("pointerdown", this.handlePointerDown, this);
|
||||
this.scene.input.on("pointerup", this.handlePointerUp, this);
|
||||
}
|
||||
|
||||
update(): void {
|
||||
@@ -5918,9 +5907,9 @@ class InputManager {
|
||||
class AssetManager {
|
||||
loadAssets(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.scene.load.on('filecomplete', this.handleFileComplete, this);
|
||||
this.scene.load.on('loaderror', this.handleLoadError, this);
|
||||
this.scene.load.on('complete', () => resolve());
|
||||
this.scene.load.on("filecomplete", this.handleFileComplete, this);
|
||||
this.scene.load.on("loaderror", this.handleLoadError, this);
|
||||
this.scene.load.on("complete", () => resolve());
|
||||
|
||||
this.scene.load.start();
|
||||
});
|
||||
@@ -5936,8 +5925,8 @@ class AssetManager {
|
||||
private loadFallbackAsset(key: string): void {
|
||||
// Load placeholder or default assets
|
||||
switch (key) {
|
||||
case 'player':
|
||||
this.scene.load.image('player', 'assets/defaults/default-player.png');
|
||||
case "player":
|
||||
this.scene.load.image("player", "assets/defaults/default-player.png");
|
||||
break;
|
||||
default:
|
||||
console.warn(`No fallback for asset: ${key}`);
|
||||
@@ -5964,11 +5953,11 @@ class GameSystem {
|
||||
|
||||
private attemptRecovery(context: string): void {
|
||||
switch (context) {
|
||||
case 'update':
|
||||
case "update":
|
||||
// Reset system state
|
||||
this.reset();
|
||||
break;
|
||||
case 'render':
|
||||
case "render":
|
||||
// Disable visual effects
|
||||
this.disableEffects();
|
||||
break;
|
||||
@@ -5988,7 +5977,7 @@ class GameSystem {
|
||||
|
||||
```typescript
|
||||
// Example test for game mechanics
|
||||
describe('HealthComponent', () => {
|
||||
describe("HealthComponent", () => {
|
||||
let healthComponent: HealthComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -5996,18 +5985,18 @@ describe('HealthComponent', () => {
|
||||
healthComponent = new HealthComponent(mockEntity, 100);
|
||||
});
|
||||
|
||||
test('should initialize with correct health', () => {
|
||||
test("should initialize with correct health", () => {
|
||||
expect(healthComponent.currentHealth).toBe(100);
|
||||
expect(healthComponent.maxHealth).toBe(100);
|
||||
});
|
||||
|
||||
test('should handle damage correctly', () => {
|
||||
test("should handle damage correctly", () => {
|
||||
healthComponent.takeDamage(25);
|
||||
expect(healthComponent.currentHealth).toBe(75);
|
||||
expect(healthComponent.isAlive()).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle death correctly', () => {
|
||||
test("should handle death correctly", () => {
|
||||
healthComponent.takeDamage(150);
|
||||
expect(healthComponent.currentHealth).toBe(0);
|
||||
expect(healthComponent.isAlive()).toBe(false);
|
||||
@@ -6020,7 +6009,7 @@ describe('HealthComponent', () => {
|
||||
**Scene Testing:**
|
||||
|
||||
```typescript
|
||||
describe('GameScene Integration', () => {
|
||||
describe("GameScene Integration", () => {
|
||||
let scene: GameScene;
|
||||
let mockGame: Phaser.Game;
|
||||
|
||||
@@ -6030,7 +6019,7 @@ describe('GameScene Integration', () => {
|
||||
scene = new GameScene();
|
||||
});
|
||||
|
||||
test('should initialize all systems', () => {
|
||||
test("should initialize all systems", () => {
|
||||
scene.create({});
|
||||
|
||||
expect(scene.gameManager).toBeDefined();
|
||||
@@ -9704,21 +9693,21 @@ workflow:
|
||||
- brainstorming_session
|
||||
- game_research_prompt
|
||||
- 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
|
||||
creates: game-design-doc.md
|
||||
requires: game-brief.md
|
||||
optional_steps:
|
||||
- competitive_analysis
|
||||
- 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
|
||||
creates: level-design-doc.md
|
||||
requires: game-design-doc.md
|
||||
optional_steps:
|
||||
- level_prototyping
|
||||
- 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
|
||||
creates: game-architecture.md
|
||||
requires:
|
||||
@@ -9728,7 +9717,7 @@ workflow:
|
||||
- technical_research_prompt
|
||||
- performance_analysis
|
||||
- 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
|
||||
validates: design_consistency
|
||||
requires: all_design_documents
|
||||
@@ -9753,7 +9742,7 @@ workflow:
|
||||
optional_steps:
|
||||
- quick_brainstorming
|
||||
- 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
|
||||
creates: prototype-design.md
|
||||
uses: create-doc prototype-design OR create-game-story
|
||||
@@ -9917,7 +9906,7 @@ workflow:
|
||||
notes: Implement stories in priority order. Test frequently and adjust design based on what feels fun. Document discoveries.
|
||||
workflow_end:
|
||||
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:
|
||||
- step: jam_concept
|
||||
agent: game-designer
|
||||
@@ -10377,7 +10366,7 @@ interface GameState {
|
||||
interface GameSettings {
|
||||
musicVolume: number;
|
||||
sfxVolume: number;
|
||||
difficulty: 'easy' | 'normal' | 'hard';
|
||||
difficulty: "easy" | "normal" | "hard";
|
||||
controls: ControlScheme;
|
||||
}
|
||||
```
|
||||
@@ -10418,12 +10407,12 @@ class GameScene extends Phaser.Scene {
|
||||
private inputManager!: InputManager;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
super({ key: "GameScene" });
|
||||
}
|
||||
|
||||
preload(): void {
|
||||
// Load only scene-specific assets
|
||||
this.load.image('player', 'assets/player.png');
|
||||
this.load.image("player", "assets/player.png");
|
||||
}
|
||||
|
||||
create(data: SceneData): void {
|
||||
@@ -10448,7 +10437,7 @@ class GameScene extends Phaser.Scene {
|
||||
this.inputManager.destroy();
|
||||
|
||||
// Remove event listeners
|
||||
this.events.off('*');
|
||||
this.events.off("*");
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -10457,13 +10446,13 @@ class GameScene extends Phaser.Scene {
|
||||
|
||||
```typescript
|
||||
// Proper scene transitions with data
|
||||
this.scene.start('NextScene', {
|
||||
this.scene.start("NextScene", {
|
||||
playerScore: this.playerScore,
|
||||
currentLevel: this.currentLevel + 1,
|
||||
});
|
||||
|
||||
// Scene overlays for UI
|
||||
this.scene.launch('PauseMenuScene');
|
||||
this.scene.launch("PauseMenuScene");
|
||||
this.scene.pause();
|
||||
```
|
||||
|
||||
@@ -10507,7 +10496,7 @@ class Player extends GameEntity {
|
||||
private health!: HealthComponent;
|
||||
|
||||
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.health = this.addComponent(new HealthComponent(this, 100));
|
||||
@@ -10527,7 +10516,7 @@ class GameManager {
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
if (GameManager.instance) {
|
||||
throw new Error('GameManager already exists!');
|
||||
throw new Error("GameManager already exists!");
|
||||
}
|
||||
|
||||
this.scene = scene;
|
||||
@@ -10537,7 +10526,7 @@ class GameManager {
|
||||
|
||||
static getInstance(): GameManager {
|
||||
if (!GameManager.instance) {
|
||||
throw new Error('GameManager not initialized!');
|
||||
throw new Error("GameManager not initialized!");
|
||||
}
|
||||
return GameManager.instance;
|
||||
}
|
||||
@@ -10584,7 +10573,7 @@ class BulletPool {
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -10684,12 +10673,14 @@ class InputManager {
|
||||
}
|
||||
|
||||
private setupKeyboard(): void {
|
||||
this.keys = this.scene.input.keyboard.addKeys('W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT');
|
||||
this.keys = this.scene.input.keyboard.addKeys(
|
||||
"W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT",
|
||||
);
|
||||
}
|
||||
|
||||
private setupTouch(): void {
|
||||
this.scene.input.on('pointerdown', this.handlePointerDown, this);
|
||||
this.scene.input.on('pointerup', this.handlePointerUp, this);
|
||||
this.scene.input.on("pointerdown", this.handlePointerDown, this);
|
||||
this.scene.input.on("pointerup", this.handlePointerUp, this);
|
||||
}
|
||||
|
||||
update(): void {
|
||||
@@ -10716,9 +10707,9 @@ class InputManager {
|
||||
class AssetManager {
|
||||
loadAssets(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.scene.load.on('filecomplete', this.handleFileComplete, this);
|
||||
this.scene.load.on('loaderror', this.handleLoadError, this);
|
||||
this.scene.load.on('complete', () => resolve());
|
||||
this.scene.load.on("filecomplete", this.handleFileComplete, this);
|
||||
this.scene.load.on("loaderror", this.handleLoadError, this);
|
||||
this.scene.load.on("complete", () => resolve());
|
||||
|
||||
this.scene.load.start();
|
||||
});
|
||||
@@ -10734,8 +10725,8 @@ class AssetManager {
|
||||
private loadFallbackAsset(key: string): void {
|
||||
// Load placeholder or default assets
|
||||
switch (key) {
|
||||
case 'player':
|
||||
this.scene.load.image('player', 'assets/defaults/default-player.png');
|
||||
case "player":
|
||||
this.scene.load.image("player", "assets/defaults/default-player.png");
|
||||
break;
|
||||
default:
|
||||
console.warn(`No fallback for asset: ${key}`);
|
||||
@@ -10762,11 +10753,11 @@ class GameSystem {
|
||||
|
||||
private attemptRecovery(context: string): void {
|
||||
switch (context) {
|
||||
case 'update':
|
||||
case "update":
|
||||
// Reset system state
|
||||
this.reset();
|
||||
break;
|
||||
case 'render':
|
||||
case "render":
|
||||
// Disable visual effects
|
||||
this.disableEffects();
|
||||
break;
|
||||
@@ -10786,7 +10777,7 @@ class GameSystem {
|
||||
|
||||
```typescript
|
||||
// Example test for game mechanics
|
||||
describe('HealthComponent', () => {
|
||||
describe("HealthComponent", () => {
|
||||
let healthComponent: HealthComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -10794,18 +10785,18 @@ describe('HealthComponent', () => {
|
||||
healthComponent = new HealthComponent(mockEntity, 100);
|
||||
});
|
||||
|
||||
test('should initialize with correct health', () => {
|
||||
test("should initialize with correct health", () => {
|
||||
expect(healthComponent.currentHealth).toBe(100);
|
||||
expect(healthComponent.maxHealth).toBe(100);
|
||||
});
|
||||
|
||||
test('should handle damage correctly', () => {
|
||||
test("should handle damage correctly", () => {
|
||||
healthComponent.takeDamage(25);
|
||||
expect(healthComponent.currentHealth).toBe(75);
|
||||
expect(healthComponent.isAlive()).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle death correctly', () => {
|
||||
test("should handle death correctly", () => {
|
||||
healthComponent.takeDamage(150);
|
||||
expect(healthComponent.currentHealth).toBe(0);
|
||||
expect(healthComponent.isAlive()).toBe(false);
|
||||
@@ -10818,7 +10809,7 @@ describe('HealthComponent', () => {
|
||||
**Scene Testing:**
|
||||
|
||||
```typescript
|
||||
describe('GameScene Integration', () => {
|
||||
describe("GameScene Integration", () => {
|
||||
let scene: GameScene;
|
||||
let mockGame: Phaser.Game;
|
||||
|
||||
@@ -10828,7 +10819,7 @@ describe('GameScene Integration', () => {
|
||||
scene = new GameScene();
|
||||
});
|
||||
|
||||
test('should initialize all systems', () => {
|
||||
test("should initialize all systems", () => {
|
||||
scene.create({});
|
||||
|
||||
expect(scene.gameManager).toBeDefined();
|
||||
|
||||
@@ -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`
|
||||
- **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`
|
||||
- **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.
|
||||
|
||||
@@ -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`
|
||||
- **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`
|
||||
- **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.
|
||||
|
||||
@@ -484,7 +484,7 @@ dependencies:
|
||||
==================== START: .bmad-2d-unity-game-dev/tasks/facilitate-brainstorming-session.md ====================
|
||||
---
|
||||
docOutputLocation: docs/brainstorming-session-results.md
|
||||
template: '.bmad-2d-unity-game-dev/templates/brainstorming-output-tmpl.yaml'
|
||||
template: ".bmad-2d-unity-game-dev/templates/brainstorming-output-tmpl.yaml"
|
||||
---
|
||||
|
||||
# Facilitate Brainstorming Session Task
|
||||
@@ -2087,14 +2087,7 @@ sections:
|
||||
title: Feature Comparison Matrix
|
||||
instruction: Create a detailed comparison table of key features across competitors
|
||||
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:
|
||||
- category: "Core Functionality"
|
||||
items:
|
||||
@@ -2106,13 +2099,7 @@ sections:
|
||||
- ["Onboarding Time", "{{time}}", "{{time}}", "{{time}}", "{{time}}"]
|
||||
- category: "Integration & Ecosystem"
|
||||
items:
|
||||
- [
|
||||
"API Availability",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
]
|
||||
- ["API Availability", "{{availability}}", "{{availability}}", "{{availability}}", "{{availability}}"]
|
||||
- ["Third-party Integrations", "{{number}}", "{{number}}", "{{number}}", "{{number}}"]
|
||||
- category: "Pricing & Plans"
|
||||
items:
|
||||
@@ -2870,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`
|
||||
- **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`
|
||||
- **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.
|
||||
@@ -13670,21 +13657,21 @@ workflow:
|
||||
- brainstorming_session
|
||||
- game_research_prompt
|
||||
- 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
|
||||
creates: game-design-doc.md
|
||||
requires: game-brief.md
|
||||
optional_steps:
|
||||
- competitive_analysis
|
||||
- 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
|
||||
creates: level-design-doc.md
|
||||
requires: game-design-doc.md
|
||||
optional_steps:
|
||||
- level_prototyping
|
||||
- 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
|
||||
creates: game-architecture.md
|
||||
requires:
|
||||
@@ -13694,7 +13681,7 @@ workflow:
|
||||
- technical_research_prompt
|
||||
- performance_analysis
|
||||
- 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
|
||||
validates: design_consistency
|
||||
requires: all_design_documents
|
||||
@@ -13719,7 +13706,7 @@ workflow:
|
||||
optional_steps:
|
||||
- quick_brainstorming
|
||||
- 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
|
||||
creates: prototype-design.md
|
||||
uses: create-doc prototype-design OR create-game-story
|
||||
@@ -13883,7 +13870,7 @@ workflow:
|
||||
notes: Implement stories in priority order. Test frequently in the Unity Editor and adjust design based on what feels fun. Document discoveries.
|
||||
workflow_end:
|
||||
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:
|
||||
- step: jam_concept
|
||||
agent: game-designer
|
||||
@@ -14473,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`
|
||||
- **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`
|
||||
- **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.
|
||||
|
||||
1038
dist/teams/team-all.txt
vendored
1038
dist/teams/team-all.txt
vendored
File diff suppressed because it is too large
Load Diff
177
dist/teams/team-fullstack.txt
vendored
177
dist/teams/team-fullstack.txt
vendored
@@ -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`)
|
||||
- **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`)
|
||||
- **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.
|
||||
@@ -1838,7 +1838,7 @@ Agents should be workflow-aware: know active workflow, their role, access artifa
|
||||
==================== START: .bmad-core/tasks/facilitate-brainstorming-session.md ====================
|
||||
---
|
||||
docOutputLocation: docs/brainstorming-session-results.md
|
||||
template: '.bmad-core/templates/brainstorming-output-tmpl.yaml'
|
||||
template: ".bmad-core/templates/brainstorming-output-tmpl.yaml"
|
||||
---
|
||||
|
||||
# Facilitate Brainstorming Session Task
|
||||
@@ -3224,14 +3224,7 @@ sections:
|
||||
title: Feature Comparison Matrix
|
||||
instruction: Create a detailed comparison table of key features across competitors
|
||||
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:
|
||||
- category: "Core Functionality"
|
||||
items:
|
||||
@@ -3243,13 +3236,7 @@ sections:
|
||||
- ["Onboarding Time", "{{time}}", "{{time}}", "{{time}}", "{{time}}"]
|
||||
- category: "Integration & Ecosystem"
|
||||
items:
|
||||
- [
|
||||
"API Availability",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
]
|
||||
- ["API Availability", "{{availability}}", "{{availability}}", "{{availability}}", "{{availability}}"]
|
||||
- ["Third-party Integrations", "{{number}}", "{{number}}", "{{number}}", "{{number}}"]
|
||||
- category: "Pricing & Plans"
|
||||
items:
|
||||
@@ -6439,24 +6426,12 @@ sections:
|
||||
rows:
|
||||
- ["Framework", "{{framework}}", "{{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}}"]
|
||||
- ["Build Tool", "{{build_tool}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Styling", "{{styling_solution}}", "{{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}}"]
|
||||
- ["Animation", "{{animation_lib}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Dev Tools", "{{dev_tools}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
@@ -6741,29 +6716,11 @@ sections:
|
||||
columns: [Category, Technology, Version, Purpose, Rationale]
|
||||
rows:
|
||||
- ["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}}"]
|
||||
- ["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}}"]
|
||||
- ["Database", "{{database}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Cache", "{{cache}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
@@ -7091,60 +7048,60 @@ sections:
|
||||
type: code
|
||||
language: plaintext
|
||||
examples:
|
||||
- |
|
||||
{{project-name}}/
|
||||
├── .github/ # CI/CD workflows
|
||||
│ └── workflows/
|
||||
│ ├── ci.yaml
|
||||
│ └── deploy.yaml
|
||||
├── apps/ # Application packages
|
||||
│ ├── web/ # Frontend application
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # UI components
|
||||
│ │ │ ├── pages/ # Page components/routes
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API client services
|
||||
│ │ │ ├── stores/ # State management
|
||||
│ │ │ ├── styles/ # Global styles/themes
|
||||
│ │ │ └── utils/ # Frontend utilities
|
||||
│ │ ├── public/ # Static assets
|
||||
│ │ ├── tests/ # Frontend tests
|
||||
│ │ └── package.json
|
||||
│ └── api/ # Backend application
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API routes/controllers
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ ├── middleware/ # Express/API middleware
|
||||
│ │ ├── utils/ # Backend utilities
|
||||
│ │ └── {{serverless_or_server_entry}}
|
||||
│ ├── tests/ # Backend tests
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
│ ├── shared/ # Shared types/utilities
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── types/ # TypeScript interfaces
|
||||
│ │ │ ├── constants/ # Shared constants
|
||||
│ │ │ └── utils/ # Shared utilities
|
||||
│ │ └── package.json
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── config/ # Shared configuration
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── jest/
|
||||
├── infrastructure/ # IaC definitions
|
||||
│ └── {{iac_structure}}
|
||||
├── scripts/ # Build/deploy scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── prd.md
|
||||
│ ├── front-end-spec.md
|
||||
│ └── fullstack-architecture.md
|
||||
├── .env.example # Environment template
|
||||
├── package.json # Root package.json
|
||||
├── {{monorepo_config}} # Monorepo configuration
|
||||
└── README.md
|
||||
- |
|
||||
{{project-name}}/
|
||||
├── .github/ # CI/CD workflows
|
||||
│ └── workflows/
|
||||
│ ├── ci.yaml
|
||||
│ └── deploy.yaml
|
||||
├── apps/ # Application packages
|
||||
│ ├── web/ # Frontend application
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # UI components
|
||||
│ │ │ ├── pages/ # Page components/routes
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API client services
|
||||
│ │ │ ├── stores/ # State management
|
||||
│ │ │ ├── styles/ # Global styles/themes
|
||||
│ │ │ └── utils/ # Frontend utilities
|
||||
│ │ ├── public/ # Static assets
|
||||
│ │ ├── tests/ # Frontend tests
|
||||
│ │ └── package.json
|
||||
│ └── api/ # Backend application
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API routes/controllers
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ ├── middleware/ # Express/API middleware
|
||||
│ │ ├── utils/ # Backend utilities
|
||||
│ │ └── {{serverless_or_server_entry}}
|
||||
│ ├── tests/ # Backend tests
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
│ ├── shared/ # Shared types/utilities
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── types/ # TypeScript interfaces
|
||||
│ │ │ ├── constants/ # Shared constants
|
||||
│ │ │ └── utils/ # Shared utilities
|
||||
│ │ └── package.json
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── config/ # Shared configuration
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── jest/
|
||||
├── infrastructure/ # IaC definitions
|
||||
│ └── {{iac_structure}}
|
||||
├── scripts/ # Build/deploy scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── prd.md
|
||||
│ ├── front-end-spec.md
|
||||
│ └── fullstack-architecture.md
|
||||
├── .env.example # Environment template
|
||||
├── package.json # Root package.json
|
||||
├── {{monorepo_config}} # Monorepo configuration
|
||||
└── README.md
|
||||
|
||||
- id: development-workflow
|
||||
title: Development Workflow
|
||||
@@ -7274,10 +7231,10 @@ sections:
|
||||
type: code
|
||||
language: text
|
||||
template: |
|
||||
E2E Tests
|
||||
/ \
|
||||
Integration Tests
|
||||
/ \
|
||||
E2E Tests
|
||||
/ \
|
||||
Integration Tests
|
||||
/ \
|
||||
Frontend Unit Backend Unit
|
||||
- id: test-organization
|
||||
title: Test Organization
|
||||
|
||||
861
dist/teams/team-ide-minimal.txt
vendored
861
dist/teams/team-ide-minimal.txt
vendored
File diff suppressed because it is too large
Load Diff
177
dist/teams/team-no-ui.txt
vendored
177
dist/teams/team-no-ui.txt
vendored
@@ -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`)
|
||||
- **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`)
|
||||
- **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.
|
||||
@@ -1784,7 +1784,7 @@ Agents should be workflow-aware: know active workflow, their role, access artifa
|
||||
==================== START: .bmad-core/tasks/facilitate-brainstorming-session.md ====================
|
||||
---
|
||||
docOutputLocation: docs/brainstorming-session-results.md
|
||||
template: '.bmad-core/templates/brainstorming-output-tmpl.yaml'
|
||||
template: ".bmad-core/templates/brainstorming-output-tmpl.yaml"
|
||||
---
|
||||
|
||||
# Facilitate Brainstorming Session Task
|
||||
@@ -3170,14 +3170,7 @@ sections:
|
||||
title: Feature Comparison Matrix
|
||||
instruction: Create a detailed comparison table of key features across competitors
|
||||
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:
|
||||
- category: "Core Functionality"
|
||||
items:
|
||||
@@ -3189,13 +3182,7 @@ sections:
|
||||
- ["Onboarding Time", "{{time}}", "{{time}}", "{{time}}", "{{time}}"]
|
||||
- category: "Integration & Ecosystem"
|
||||
items:
|
||||
- [
|
||||
"API Availability",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
"{{availability}}",
|
||||
]
|
||||
- ["API Availability", "{{availability}}", "{{availability}}", "{{availability}}", "{{availability}}"]
|
||||
- ["Third-party Integrations", "{{number}}", "{{number}}", "{{number}}", "{{number}}"]
|
||||
- category: "Pricing & Plans"
|
||||
items:
|
||||
@@ -5979,24 +5966,12 @@ sections:
|
||||
rows:
|
||||
- ["Framework", "{{framework}}", "{{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}}"]
|
||||
- ["Build Tool", "{{build_tool}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Styling", "{{styling_solution}}", "{{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}}"]
|
||||
- ["Animation", "{{animation_lib}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Dev Tools", "{{dev_tools}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
@@ -6281,29 +6256,11 @@ sections:
|
||||
columns: [Category, Technology, Version, Purpose, Rationale]
|
||||
rows:
|
||||
- ["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}}"]
|
||||
- ["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}}"]
|
||||
- ["Database", "{{database}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
- ["Cache", "{{cache}}", "{{version}}", "{{purpose}}", "{{why_chosen}}"]
|
||||
@@ -6631,60 +6588,60 @@ sections:
|
||||
type: code
|
||||
language: plaintext
|
||||
examples:
|
||||
- |
|
||||
{{project-name}}/
|
||||
├── .github/ # CI/CD workflows
|
||||
│ └── workflows/
|
||||
│ ├── ci.yaml
|
||||
│ └── deploy.yaml
|
||||
├── apps/ # Application packages
|
||||
│ ├── web/ # Frontend application
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # UI components
|
||||
│ │ │ ├── pages/ # Page components/routes
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API client services
|
||||
│ │ │ ├── stores/ # State management
|
||||
│ │ │ ├── styles/ # Global styles/themes
|
||||
│ │ │ └── utils/ # Frontend utilities
|
||||
│ │ ├── public/ # Static assets
|
||||
│ │ ├── tests/ # Frontend tests
|
||||
│ │ └── package.json
|
||||
│ └── api/ # Backend application
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API routes/controllers
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ ├── middleware/ # Express/API middleware
|
||||
│ │ ├── utils/ # Backend utilities
|
||||
│ │ └── {{serverless_or_server_entry}}
|
||||
│ ├── tests/ # Backend tests
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
│ ├── shared/ # Shared types/utilities
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── types/ # TypeScript interfaces
|
||||
│ │ │ ├── constants/ # Shared constants
|
||||
│ │ │ └── utils/ # Shared utilities
|
||||
│ │ └── package.json
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── config/ # Shared configuration
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── jest/
|
||||
├── infrastructure/ # IaC definitions
|
||||
│ └── {{iac_structure}}
|
||||
├── scripts/ # Build/deploy scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── prd.md
|
||||
│ ├── front-end-spec.md
|
||||
│ └── fullstack-architecture.md
|
||||
├── .env.example # Environment template
|
||||
├── package.json # Root package.json
|
||||
├── {{monorepo_config}} # Monorepo configuration
|
||||
└── README.md
|
||||
- |
|
||||
{{project-name}}/
|
||||
├── .github/ # CI/CD workflows
|
||||
│ └── workflows/
|
||||
│ ├── ci.yaml
|
||||
│ └── deploy.yaml
|
||||
├── apps/ # Application packages
|
||||
│ ├── web/ # Frontend application
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # UI components
|
||||
│ │ │ ├── pages/ # Page components/routes
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API client services
|
||||
│ │ │ ├── stores/ # State management
|
||||
│ │ │ ├── styles/ # Global styles/themes
|
||||
│ │ │ └── utils/ # Frontend utilities
|
||||
│ │ ├── public/ # Static assets
|
||||
│ │ ├── tests/ # Frontend tests
|
||||
│ │ └── package.json
|
||||
│ └── api/ # Backend application
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API routes/controllers
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ ├── middleware/ # Express/API middleware
|
||||
│ │ ├── utils/ # Backend utilities
|
||||
│ │ └── {{serverless_or_server_entry}}
|
||||
│ ├── tests/ # Backend tests
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
│ ├── shared/ # Shared types/utilities
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── types/ # TypeScript interfaces
|
||||
│ │ │ ├── constants/ # Shared constants
|
||||
│ │ │ └── utils/ # Shared utilities
|
||||
│ │ └── package.json
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── config/ # Shared configuration
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── jest/
|
||||
├── infrastructure/ # IaC definitions
|
||||
│ └── {{iac_structure}}
|
||||
├── scripts/ # Build/deploy scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── prd.md
|
||||
│ ├── front-end-spec.md
|
||||
│ └── fullstack-architecture.md
|
||||
├── .env.example # Environment template
|
||||
├── package.json # Root package.json
|
||||
├── {{monorepo_config}} # Monorepo configuration
|
||||
└── README.md
|
||||
|
||||
- id: development-workflow
|
||||
title: Development Workflow
|
||||
@@ -6814,10 +6771,10 @@ sections:
|
||||
type: code
|
||||
language: text
|
||||
template: |
|
||||
E2E Tests
|
||||
/ \
|
||||
Integration Tests
|
||||
/ \
|
||||
E2E Tests
|
||||
/ \
|
||||
Integration Tests
|
||||
/ \
|
||||
Frontend Unit Backend Unit
|
||||
- id: test-organization
|
||||
title: Test Organization
|
||||
|
||||
@@ -29,14 +29,14 @@ The Test Architect (Quinn) provides comprehensive quality assurance throughout t
|
||||
|
||||
### Quick Command Reference
|
||||
|
||||
| **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 |
|
||||
| | `*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 |
|
||||
| | `*nfr` | Validate quality attributes | `docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md` | High for critical features |
|
||||
| **After Development** | `*review` | Comprehensive assessment | QA Results in story + `docs/qa/gates/{epic}.{story}-{slug}.yml` | **Required** |
|
||||
| **Post-Review** | `*gate` | Update quality decision | Updated `docs/qa/gates/{epic}.{story}-{slug}.yml` | As needed |
|
||||
| **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 |
|
||||
| | `*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 |
|
||||
| | `*nfr` | Validate quality attributes | `docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md` | High for critical features |
|
||||
| **After Development** | `*review` | Comprehensive assessment | QA Results in story + `docs/qa/gates/{epic}.{story}-{slug}.yml` | **Required** |
|
||||
| **Post-Review** | `*gate` | Update quality decision | Updated `docs/qa/gates/{epic}.{story}-{slug}.yml` | As needed |
|
||||
|
||||
### Stage 1: After Story Creation (Before Dev Starts)
|
||||
|
||||
@@ -134,24 +134,24 @@ The Test Architect (Quinn) provides comprehensive quality assurance throughout t
|
||||
|
||||
### Understanding Gate Decisions
|
||||
|
||||
| **Status** | **Meaning** | **Action Required** | **Can Proceed?** |
|
||||
| ------------ | -------------------------------------------- | ----------------------- | ---------------- |
|
||||
| **PASS** | All critical requirements met | None | ✅ Yes |
|
||||
| **CONCERNS** | Non-critical issues found | Team review recommended | ⚠️ With caution |
|
||||
| **FAIL** | Critical issues (security, missing P0 tests) | Must fix | ❌ No |
|
||||
| **WAIVED** | Issues acknowledged and accepted | Document reasoning | ✅ With approval |
|
||||
| **Status** | **Meaning** | **Action Required** | **Can Proceed?** |
|
||||
|------------|-------------|-------------------|------------------|
|
||||
| **PASS** | All critical requirements met | None | ✅ Yes |
|
||||
| **CONCERNS** | Non-critical issues found | Team review recommended | ⚠️ With caution |
|
||||
| **FAIL** | Critical issues (security, missing P0 tests) | Must fix | ❌ No |
|
||||
| **WAIVED** | Issues acknowledged and accepted | Document reasoning | ✅ With approval |
|
||||
|
||||
### Risk-Based Testing Strategy
|
||||
|
||||
The Test Architect uses risk scoring to prioritize testing:
|
||||
|
||||
| **Risk Score** | **Calculation** | **Testing Priority** | **Gate Impact** |
|
||||
| -------------- | ------------------------------ | ------------------------- | ------------------------ |
|
||||
| **9** | High probability × High impact | P0 - Must test thoroughly | FAIL if untested |
|
||||
| **6** | Medium-high combinations | P1 - Should test well | CONCERNS if gaps |
|
||||
| **4** | Medium combinations | P1 - Should test | CONCERNS if notable gaps |
|
||||
| **2-3** | Low-medium combinations | P2 - Nice to have | Note in review |
|
||||
| **1** | Minimal risk | P2 - Minimal | Note in review |
|
||||
| **Risk Score** | **Calculation** | **Testing Priority** | **Gate Impact** |
|
||||
|---------------|----------------|-------------------|----------------|
|
||||
| **9** | High probability × High impact | P0 - Must test thoroughly | FAIL if untested |
|
||||
| **6** | Medium-high combinations | P1 - Should test well | CONCERNS if gaps |
|
||||
| **4** | Medium combinations | P1 - Should test | CONCERNS if notable gaps |
|
||||
| **2-3** | Low-medium combinations | P2 - Nice to have | Note in review |
|
||||
| **1** | Minimal risk | P2 - Minimal | Note in review |
|
||||
|
||||
### Special Situations & Best Practices
|
||||
|
||||
@@ -227,14 +227,14 @@ All Test Architect activities create permanent records:
|
||||
|
||||
**Should I run Test Architect commands?**
|
||||
|
||||
| **Scenario** | **Before Dev** | **During Dev** | **After Dev** |
|
||||
| ------------------------ | ------------------------------- | ---------------------------- | ---------------------------- |
|
||||
| **Simple bug fix** | Optional | Optional | Required `*review` |
|
||||
| **New feature** | Recommended `*risk`, `*design` | Optional `*trace` | Required `*review` |
|
||||
| **Brownfield change** | **Required** `*risk`, `*design` | Recommended `*trace`, `*nfr` | Required `*review` |
|
||||
| **API modification** | **Required** `*risk`, `*design` | **Required** `*trace` | Required `*review` |
|
||||
| **Performance-critical** | Recommended `*design` | **Required** `*nfr` | Required `*review` |
|
||||
| **Data migration** | **Required** `*risk`, `*design` | **Required** `*trace` | Required `*review` + `*gate` |
|
||||
| **Scenario** | **Before Dev** | **During Dev** | **After Dev** |
|
||||
|-------------|---------------|----------------|---------------|
|
||||
| **Simple bug fix** | Optional | Optional | Required `*review` |
|
||||
| **New feature** | Recommended `*risk`, `*design` | Optional `*trace` | Required `*review` |
|
||||
| **Brownfield change** | **Required** `*risk`, `*design` | Recommended `*trace`, `*nfr` | Required `*review` |
|
||||
| **API modification** | **Required** `*risk`, `*design` | **Required** `*trace` | Required `*review` |
|
||||
| **Performance-critical** | Recommended `*design` | **Required** `*nfr` | Required `*review` |
|
||||
| **Data migration** | **Required** `*risk`, `*design` | **Required** `*trace` | Required `*review` + `*gate` |
|
||||
|
||||
### Success Metrics
|
||||
|
||||
|
||||
@@ -376,14 +376,14 @@ Manages quality gate decisions:
|
||||
|
||||
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** |
|
||||
| ------------------ | ----------- | ----------------------- | -------------------------- | -------------------------------------------------------------- |
|
||||
| **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` |
|
||||
| **Development** | `*trace` | Mid-implementation | Verify test coverage | `docs/qa/assessments/{epic}.{story}-trace-{YYYYMMDD}.md` |
|
||||
| | `*nfr` | While building features | Catch quality issues early | `docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md` |
|
||||
| **Review** | `*review` | Story marked complete | Full quality assessment | QA Results in story + gate file |
|
||||
| **Post-Review** | `*gate` | After fixing issues | Update quality decision | Updated `docs/qa/gates/{epic}.{story}-{slug}.yml` |
|
||||
| **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` |
|
||||
| | `*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` |
|
||||
| | `*nfr` | While building features | Catch quality issues early | `docs/qa/assessments/{epic}.{story}-nfr-{YYYYMMDD}.md` |
|
||||
| **Review** | `*review` | Story marked complete | Full quality assessment | QA Results in story + gate file |
|
||||
| **Post-Review** | `*gate` | After fixing issues | Update quality decision | Updated `docs/qa/gates/{epic}.{story}-{slug}.yml` |
|
||||
|
||||
#### Example Commands
|
||||
|
||||
|
||||
@@ -1,147 +1,77 @@
|
||||
# Versioning and Releases
|
||||
# How to Release a New Version
|
||||
|
||||
BMad Method uses a simplified release system with manual control and automatic release notes generation.
|
||||
## Automated Releases (Recommended)
|
||||
|
||||
## 🚀 Release Workflow
|
||||
The easiest way to release new versions is through **automatic semantic releases**. Just commit with the right message format and push and everything else happens automatically.
|
||||
|
||||
### Command Line Release (Recommended)
|
||||
### Commit Message Format
|
||||
|
||||
The fastest way to create a release with beautiful release notes:
|
||||
Use these prefixes to control what type of release happens:
|
||||
|
||||
```bash
|
||||
# Preview what will be in the release
|
||||
npm run preview:release
|
||||
|
||||
# Create a release
|
||||
npm run release:patch # 5.1.0 → 5.1.1 (bug fixes)
|
||||
npm run release:minor # 5.1.0 → 5.2.0 (new features)
|
||||
npm run release:major # 5.1.0 → 6.0.0 (breaking changes)
|
||||
|
||||
# Watch the release process
|
||||
npm run release:watch
|
||||
fix: resolve CLI argument parsing bug # → patch release (4.1.0 → 4.1.1)
|
||||
feat: add new agent orchestration mode # → minor release (4.1.0 → 4.2.0)
|
||||
feat!: redesign CLI interface # → major release (4.1.0 → 5.0.0)
|
||||
```
|
||||
|
||||
### One-Liner Release
|
||||
### What Happens Automatically
|
||||
|
||||
When you push commits with `fix:` or `feat:`, GitHub Actions will:
|
||||
|
||||
1. ✅ Analyze your commit messages
|
||||
2. ✅ Bump version in `package.json`
|
||||
3. ✅ Generate changelog
|
||||
4. ✅ Create git tag
|
||||
5. ✅ **Publish to NPM automatically**
|
||||
6. ✅ Create GitHub release with notes
|
||||
|
||||
### Your Simple Workflow
|
||||
|
||||
```bash
|
||||
npm run preview:release && npm run release:minor && npm run release:watch
|
||||
# Make your changes
|
||||
git add .
|
||||
git commit -m "feat: add team collaboration mode"
|
||||
git push
|
||||
|
||||
# That's it! Release happens automatically 🎉
|
||||
# Users can now run: npx bmad-method (and get the new version)
|
||||
```
|
||||
|
||||
## 📝 What Happens Automatically
|
||||
### Commits That DON'T Trigger Releases
|
||||
|
||||
When you trigger a release, the GitHub Actions workflow automatically:
|
||||
|
||||
1. ✅ **Validates** - Runs tests, linting, and formatting checks
|
||||
2. ✅ **Bumps Version** - Updates `package.json` and installer version
|
||||
3. ✅ **Generates Release Notes** - Categorizes commits since last release:
|
||||
- ✨ **New Features** (`feat:`, `Feature:`)
|
||||
- 🐛 **Bug Fixes** (`fix:`, `Fix:`)
|
||||
- 🔧 **Maintenance** (`chore:`, `Chore:`)
|
||||
- 📦 **Other Changes** (everything else)
|
||||
4. ✅ **Creates Git Tag** - Tags the release version
|
||||
5. ✅ **Publishes to NPM** - With `@latest` tag for user installations
|
||||
6. ✅ **Creates GitHub Release** - With formatted release notes
|
||||
|
||||
## 📋 Sample Release Notes
|
||||
|
||||
The workflow automatically generates professional release notes like this:
|
||||
|
||||
````markdown
|
||||
## 🚀 What's New in v5.2.0
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- feat: add team collaboration mode
|
||||
- feat: enhance CLI with interactive prompts
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- fix: resolve installation path issues
|
||||
- fix: handle edge cases in agent loading
|
||||
|
||||
### 🔧 Maintenance
|
||||
|
||||
- chore: update dependencies
|
||||
- chore: improve error messages
|
||||
|
||||
## 📦 Installation
|
||||
These commit types won't create releases (use them for maintenance):
|
||||
|
||||
```bash
|
||||
npx bmad-method install
|
||||
chore: update dependencies # No release
|
||||
docs: fix typo in readme # No release
|
||||
style: format code # No release
|
||||
test: add unit tests # No release
|
||||
```
|
||||
````
|
||||
|
||||
**Full Changelog**: https://github.com/bmadcode/BMAD-METHOD/compare/v5.1.0...v5.2.0
|
||||
|
||||
````
|
||||
|
||||
## 🎯 User Installation
|
||||
|
||||
After any release, users can immediately get the new version with:
|
||||
### Test Your Setup
|
||||
|
||||
```bash
|
||||
npx bmad-method install # Always gets latest release
|
||||
npm run release:test # Safe to run locally - tests the config
|
||||
```
|
||||
|
||||
## 📊 Preview Before Release
|
||||
---
|
||||
|
||||
Always preview what will be included in your release:
|
||||
## Manual Release Methods (Exceptions Only)
|
||||
|
||||
⚠️ Only use these methods if you need to bypass the automatic system
|
||||
|
||||
### Quick Manual Version Bump
|
||||
|
||||
```bash
|
||||
npm run preview:release
|
||||
npm run version:patch # 4.1.0 → 4.1.1 (bug fixes)
|
||||
npm run version:minor # 4.1.0 → 4.2.0 (new features)
|
||||
npm run version:major # 4.1.0 → 5.0.0 (breaking changes)
|
||||
|
||||
# Then manually publish:
|
||||
npm publish
|
||||
git push && git push --tags
|
||||
```
|
||||
|
||||
This shows:
|
||||
### Manual GitHub Actions Trigger
|
||||
|
||||
- Commits since last release
|
||||
- Categorized changes
|
||||
- Estimated next version
|
||||
- Release notes preview
|
||||
|
||||
## 🔧 Manual Release (GitHub UI)
|
||||
|
||||
You can also trigger releases through GitHub Actions:
|
||||
|
||||
1. Go to **GitHub Actions** → **Manual Release**
|
||||
2. Click **"Run workflow"**
|
||||
3. Choose version bump type (patch/minor/major)
|
||||
4. Everything else happens automatically
|
||||
|
||||
## 📈 Version Strategy
|
||||
|
||||
- **Patch** (5.1.0 → 5.1.1): Bug fixes, minor improvements
|
||||
- **Minor** (5.1.0 → 5.2.0): New features, enhancements
|
||||
- **Major** (5.1.0 → 6.0.0): Breaking changes, major redesigns
|
||||
|
||||
## 🛠️ Development Workflow
|
||||
|
||||
1. **Develop Freely** - Merge PRs to main without triggering releases
|
||||
2. **Test Unreleased Changes** - Clone repo to test latest main branch
|
||||
3. **Release When Ready** - Use command line or GitHub Actions to cut releases
|
||||
4. **Users Get Updates** - Via simple `npx bmad-method install` command
|
||||
|
||||
This gives you complete control over when releases happen while automating all the tedious parts like version bumping, release notes, and publishing.
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Check Release Status
|
||||
|
||||
```bash
|
||||
gh run list --workflow="Manual Release"
|
||||
npm view bmad-method dist-tags
|
||||
git tag -l | sort -V | tail -5
|
||||
```
|
||||
|
||||
### View Latest Release
|
||||
|
||||
```bash
|
||||
gh release view --web
|
||||
npm view bmad-method versions --json
|
||||
```
|
||||
|
||||
### If Release Fails
|
||||
|
||||
- Check GitHub Actions logs: `gh run view <run-id> --log-failed`
|
||||
- Verify NPM tokens are configured
|
||||
- Ensure branch protection allows workflow pushes
|
||||
````
|
||||
You can also trigger releases manually through GitHub Actions workflow dispatch if needed.
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import js from '@eslint/js';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier/flat';
|
||||
import nodePlugin from 'eslint-plugin-n';
|
||||
import unicorn from 'eslint-plugin-unicorn';
|
||||
import yml from 'eslint-plugin-yml';
|
||||
|
||||
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',
|
||||
// Enforce .yaml file extension for consistency
|
||||
'yml/file-extension': [
|
||||
'error',
|
||||
{
|
||||
extension: 'yaml',
|
||||
caseSensitive: true,
|
||||
},
|
||||
],
|
||||
// Prefer double quotes in YAML wherever quoting is used, but allow the other to avoid escapes
|
||||
'yml/quotes': [
|
||||
'error',
|
||||
{
|
||||
prefer: 'double',
|
||||
avoidEscape: true,
|
||||
},
|
||||
],
|
||||
// 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/**/*.yaml'],
|
||||
rules: {
|
||||
'yml/no-empty-mapping-value': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// GitHub workflow files in this repo may use empty mapping values
|
||||
{
|
||||
files: ['.github/workflows/**/*.yaml'],
|
||||
rules: {
|
||||
'yml/no-empty-mapping-value': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Other GitHub YAML files may intentionally use empty values and reserved filenames
|
||||
{
|
||||
files: ['.github/**/*.yaml'],
|
||||
rules: {
|
||||
'yml/no-empty-mapping-value': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,26 +1,26 @@
|
||||
steps:
|
||||
# Build the container image
|
||||
- name: "gcr.io/cloud-builders/docker"
|
||||
args: ["build", "-t", "gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA", "."]
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args: ['build', '-t', 'gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA', '.']
|
||||
|
||||
# Push the container image to Container Registry
|
||||
- name: "gcr.io/cloud-builders/docker"
|
||||
args: ["push", "gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA"]
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args: ['push', 'gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA']
|
||||
|
||||
# Deploy container image to Cloud Run
|
||||
- name: "gcr.io/google.com/cloudsdktool/cloud-sdk"
|
||||
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
||||
entrypoint: gcloud
|
||||
args:
|
||||
- "run"
|
||||
- "deploy"
|
||||
- "{{COMPANY_NAME}}-ai-agents"
|
||||
- "--image"
|
||||
- "gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA"
|
||||
- "--region"
|
||||
- "{{LOCATION}}"
|
||||
- "--platform"
|
||||
- "managed"
|
||||
- "--allow-unauthenticated"
|
||||
- 'run'
|
||||
- 'deploy'
|
||||
- '{{COMPANY_NAME}}-ai-agents'
|
||||
- '--image'
|
||||
- 'gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA'
|
||||
- '--region'
|
||||
- '{{LOCATION}}'
|
||||
- '--platform'
|
||||
- 'managed'
|
||||
- '--allow-unauthenticated'
|
||||
|
||||
images:
|
||||
- "gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA"
|
||||
- 'gcr.io/{{PROJECT_ID}}/{{COMPANY_NAME}}-ai-agents:$COMMIT_SHA'
|
||||
@@ -60,10 +60,10 @@ commands:
|
||||
task-execution:
|
||||
flow: Read story → Implement game feature → Write tests → Pass tests → Update [x] → Next task
|
||||
updates-ONLY:
|
||||
- 'Checkboxes: [ ] not started | [-] in progress | [x] complete'
|
||||
- 'Debug Log: | Task | File | Change | Reverted? |'
|
||||
- 'Completion Notes: Deviations only, <50 words'
|
||||
- 'Change Log: Requirement changes only'
|
||||
- "Checkboxes: [ ] not started | [-] in progress | [x] complete"
|
||||
- "Debug Log: | Task | File | Change | Reverted? |"
|
||||
- "Completion Notes: Deviations only, <50 words"
|
||||
- "Change Log: Requirement changes only"
|
||||
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
|
||||
dependencies:
|
||||
|
||||
@@ -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
|
||||
- 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 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:
|
||||
name: Jordan
|
||||
id: game-sm
|
||||
|
||||
@@ -73,7 +73,7 @@ interface GameState {
|
||||
interface GameSettings {
|
||||
musicVolume: number;
|
||||
sfxVolume: number;
|
||||
difficulty: 'easy' | 'normal' | 'hard';
|
||||
difficulty: "easy" | "normal" | "hard";
|
||||
controls: ControlScheme;
|
||||
}
|
||||
```
|
||||
@@ -114,12 +114,12 @@ class GameScene extends Phaser.Scene {
|
||||
private inputManager!: InputManager;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
super({ key: "GameScene" });
|
||||
}
|
||||
|
||||
preload(): void {
|
||||
// Load only scene-specific assets
|
||||
this.load.image('player', 'assets/player.png');
|
||||
this.load.image("player", "assets/player.png");
|
||||
}
|
||||
|
||||
create(data: SceneData): void {
|
||||
@@ -144,7 +144,7 @@ class GameScene extends Phaser.Scene {
|
||||
this.inputManager.destroy();
|
||||
|
||||
// Remove event listeners
|
||||
this.events.off('*');
|
||||
this.events.off("*");
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -153,13 +153,13 @@ class GameScene extends Phaser.Scene {
|
||||
|
||||
```typescript
|
||||
// Proper scene transitions with data
|
||||
this.scene.start('NextScene', {
|
||||
this.scene.start("NextScene", {
|
||||
playerScore: this.playerScore,
|
||||
currentLevel: this.currentLevel + 1,
|
||||
});
|
||||
|
||||
// Scene overlays for UI
|
||||
this.scene.launch('PauseMenuScene');
|
||||
this.scene.launch("PauseMenuScene");
|
||||
this.scene.pause();
|
||||
```
|
||||
|
||||
@@ -203,7 +203,7 @@ class Player extends GameEntity {
|
||||
private health!: HealthComponent;
|
||||
|
||||
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.health = this.addComponent(new HealthComponent(this, 100));
|
||||
@@ -223,7 +223,7 @@ class GameManager {
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
if (GameManager.instance) {
|
||||
throw new Error('GameManager already exists!');
|
||||
throw new Error("GameManager already exists!");
|
||||
}
|
||||
|
||||
this.scene = scene;
|
||||
@@ -233,7 +233,7 @@ class GameManager {
|
||||
|
||||
static getInstance(): GameManager {
|
||||
if (!GameManager.instance) {
|
||||
throw new Error('GameManager not initialized!');
|
||||
throw new Error("GameManager not initialized!");
|
||||
}
|
||||
return GameManager.instance;
|
||||
}
|
||||
@@ -280,7 +280,7 @@ class BulletPool {
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -380,12 +380,14 @@ class InputManager {
|
||||
}
|
||||
|
||||
private setupKeyboard(): void {
|
||||
this.keys = this.scene.input.keyboard.addKeys('W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT');
|
||||
this.keys = this.scene.input.keyboard.addKeys(
|
||||
"W,A,S,D,SPACE,ESC,UP,DOWN,LEFT,RIGHT",
|
||||
);
|
||||
}
|
||||
|
||||
private setupTouch(): void {
|
||||
this.scene.input.on('pointerdown', this.handlePointerDown, this);
|
||||
this.scene.input.on('pointerup', this.handlePointerUp, this);
|
||||
this.scene.input.on("pointerdown", this.handlePointerDown, this);
|
||||
this.scene.input.on("pointerup", this.handlePointerUp, this);
|
||||
}
|
||||
|
||||
update(): void {
|
||||
@@ -412,9 +414,9 @@ class InputManager {
|
||||
class AssetManager {
|
||||
loadAssets(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.scene.load.on('filecomplete', this.handleFileComplete, this);
|
||||
this.scene.load.on('loaderror', this.handleLoadError, this);
|
||||
this.scene.load.on('complete', () => resolve());
|
||||
this.scene.load.on("filecomplete", this.handleFileComplete, this);
|
||||
this.scene.load.on("loaderror", this.handleLoadError, this);
|
||||
this.scene.load.on("complete", () => resolve());
|
||||
|
||||
this.scene.load.start();
|
||||
});
|
||||
@@ -430,8 +432,8 @@ class AssetManager {
|
||||
private loadFallbackAsset(key: string): void {
|
||||
// Load placeholder or default assets
|
||||
switch (key) {
|
||||
case 'player':
|
||||
this.scene.load.image('player', 'assets/defaults/default-player.png');
|
||||
case "player":
|
||||
this.scene.load.image("player", "assets/defaults/default-player.png");
|
||||
break;
|
||||
default:
|
||||
console.warn(`No fallback for asset: ${key}`);
|
||||
@@ -458,11 +460,11 @@ class GameSystem {
|
||||
|
||||
private attemptRecovery(context: string): void {
|
||||
switch (context) {
|
||||
case 'update':
|
||||
case "update":
|
||||
// Reset system state
|
||||
this.reset();
|
||||
break;
|
||||
case 'render':
|
||||
case "render":
|
||||
// Disable visual effects
|
||||
this.disableEffects();
|
||||
break;
|
||||
@@ -482,7 +484,7 @@ class GameSystem {
|
||||
|
||||
```typescript
|
||||
// Example test for game mechanics
|
||||
describe('HealthComponent', () => {
|
||||
describe("HealthComponent", () => {
|
||||
let healthComponent: HealthComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -490,18 +492,18 @@ describe('HealthComponent', () => {
|
||||
healthComponent = new HealthComponent(mockEntity, 100);
|
||||
});
|
||||
|
||||
test('should initialize with correct health', () => {
|
||||
test("should initialize with correct health", () => {
|
||||
expect(healthComponent.currentHealth).toBe(100);
|
||||
expect(healthComponent.maxHealth).toBe(100);
|
||||
});
|
||||
|
||||
test('should handle damage correctly', () => {
|
||||
test("should handle damage correctly", () => {
|
||||
healthComponent.takeDamage(25);
|
||||
expect(healthComponent.currentHealth).toBe(75);
|
||||
expect(healthComponent.isAlive()).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle death correctly', () => {
|
||||
test("should handle death correctly", () => {
|
||||
healthComponent.takeDamage(150);
|
||||
expect(healthComponent.currentHealth).toBe(0);
|
||||
expect(healthComponent.isAlive()).toBe(false);
|
||||
@@ -514,7 +516,7 @@ describe('HealthComponent', () => {
|
||||
**Scene Testing:**
|
||||
|
||||
```typescript
|
||||
describe('GameScene Integration', () => {
|
||||
describe("GameScene Integration", () => {
|
||||
let scene: GameScene;
|
||||
let mockGame: Phaser.Game;
|
||||
|
||||
@@ -524,7 +526,7 @@ describe('GameScene Integration', () => {
|
||||
scene = new GameScene();
|
||||
});
|
||||
|
||||
test('should initialize all systems', () => {
|
||||
test("should initialize all systems", () => {
|
||||
scene.create({});
|
||||
|
||||
expect(scene.gameManager).toBeDefined();
|
||||
|
||||
@@ -17,21 +17,21 @@ workflow:
|
||||
- brainstorming_session
|
||||
- game_research_prompt
|
||||
- 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
|
||||
creates: game-design-doc.md
|
||||
requires: game-brief.md
|
||||
optional_steps:
|
||||
- competitive_analysis
|
||||
- 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
|
||||
creates: level-design-doc.md
|
||||
requires: game-design-doc.md
|
||||
optional_steps:
|
||||
- level_prototyping
|
||||
- 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
|
||||
creates: game-architecture.md
|
||||
requires:
|
||||
@@ -41,7 +41,7 @@ workflow:
|
||||
- technical_research_prompt
|
||||
- performance_analysis
|
||||
- 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
|
||||
validates: design_consistency
|
||||
requires: all_design_documents
|
||||
@@ -66,7 +66,7 @@ workflow:
|
||||
optional_steps:
|
||||
- quick_brainstorming
|
||||
- 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
|
||||
creates: prototype-design.md
|
||||
uses: create-doc prototype-design OR create-game-story
|
||||
|
||||
@@ -44,7 +44,7 @@ workflow:
|
||||
notes: Implement stories in priority order. Test frequently and adjust design based on what feels fun. Document discoveries.
|
||||
workflow_end:
|
||||
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:
|
||||
- step: jam_concept
|
||||
agent: game-designer
|
||||
|
||||
@@ -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.
|
||||
- exit: Say goodbye as the Game Developer, and then abandon inhabiting this persona
|
||||
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:
|
||||
- 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: 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'
|
||||
ready-for-review: 'Code matches requirements + All validations pass + Follows Unity & C# standards + File List complete + Stable FPS'
|
||||
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"
|
||||
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:
|
||||
tasks:
|
||||
|
||||
@@ -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`
|
||||
- **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`
|
||||
- **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.
|
||||
|
||||
@@ -17,21 +17,21 @@ workflow:
|
||||
- brainstorming_session
|
||||
- game_research_prompt
|
||||
- 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
|
||||
creates: game-design-doc.md
|
||||
requires: game-brief.md
|
||||
optional_steps:
|
||||
- competitive_analysis
|
||||
- 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
|
||||
creates: level-design-doc.md
|
||||
requires: game-design-doc.md
|
||||
optional_steps:
|
||||
- level_prototyping
|
||||
- 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
|
||||
creates: game-architecture.md
|
||||
requires:
|
||||
@@ -41,7 +41,7 @@ workflow:
|
||||
- technical_research_prompt
|
||||
- performance_analysis
|
||||
- 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
|
||||
validates: design_consistency
|
||||
requires: all_design_documents
|
||||
@@ -66,7 +66,7 @@ workflow:
|
||||
optional_steps:
|
||||
- quick_brainstorming
|
||||
- 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
|
||||
creates: prototype-design.md
|
||||
uses: create-doc prototype-design OR create-game-story
|
||||
|
||||
@@ -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.
|
||||
workflow_end:
|
||||
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:
|
||||
- step: jam_concept
|
||||
agent: game-designer
|
||||
|
||||
1607
package-lock.json
generated
1607
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
111
package.json
111
package.json
@@ -1,23 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "bmad-method",
|
||||
"version": "5.1.3",
|
||||
"version": "4.37.0-beta.6",
|
||||
"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",
|
||||
"bin": {
|
||||
"bmad": "tools/bmad-npx-wrapper.js",
|
||||
@@ -27,46 +11,27 @@
|
||||
"build": "node tools/cli.js build",
|
||||
"build:agents": "node tools/cli.js build --agents-only",
|
||||
"build:teams": "node tools/cli.js build --teams-only",
|
||||
"flatten": "node tools/flattener/main.js",
|
||||
"format": "prettier --write \"**/*.{js,cjs,mjs,json,md,yaml}\"",
|
||||
"format:check": "prettier --check \"**/*.{js,cjs,mjs,json,md,yaml}\"",
|
||||
"install:bmad": "node tools/installer/bin/bmad.js install",
|
||||
"lint": "eslint . --ext .js,.cjs,.mjs,.yaml --max-warnings=0",
|
||||
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
||||
"list:agents": "node tools/cli.js list:agents",
|
||||
"prepare": "husky",
|
||||
"preview:release": "node tools/preview-release-notes.js",
|
||||
"release:major": "gh workflow run \"Manual Release\" -f version_bump=major",
|
||||
"release:minor": "gh workflow run \"Manual Release\" -f version_bump=minor",
|
||||
"release:patch": "gh workflow run \"Manual Release\" -f version_bump=patch",
|
||||
"release:watch": "gh run watch",
|
||||
"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",
|
||||
"flatten": "node tools/flattener/main.js",
|
||||
"install:bmad": "node tools/installer/bin/bmad.js install",
|
||||
"format": "prettier --write \"**/*.md\"",
|
||||
"version:patch": "node tools/version-bump.js patch",
|
||||
"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"
|
||||
],
|
||||
"**/*.yaml": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"**/*.{json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
"version:major": "node tools/version-bump.js major",
|
||||
"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:test": "semantic-release --dry-run --no-ci || echo 'Config test complete - authentication errors are expected locally'",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kayvan/markdown-tree-parser": "^1.5.0",
|
||||
@@ -81,25 +46,37 @@
|
||||
"ora": "^5.4.1",
|
||||
"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": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"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",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^30.0.4",
|
||||
"lint-staged": "^16.1.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-packagejson": "^2.5.19",
|
||||
"yaml-eslint-parser": "^1.2.3",
|
||||
"semantic-release": "^22.0.0",
|
||||
"yaml-lint": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.10.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
"lint-staged": {
|
||||
"**/*.md": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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: ['*.yaml'],
|
||||
options: { singleQuote: false },
|
||||
},
|
||||
{
|
||||
files: ['*.json', '*.jsonc'],
|
||||
options: { singleQuote: false },
|
||||
},
|
||||
{
|
||||
files: ['*.cjs'],
|
||||
options: { parser: 'babel' },
|
||||
},
|
||||
],
|
||||
plugins: ['prettier-plugin-packagejson'],
|
||||
};
|
||||
@@ -5,16 +5,16 @@
|
||||
* This file ensures proper execution when run via npx from GitHub
|
||||
*/
|
||||
|
||||
const { execSync } = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Check if we're running in an npx temporary directory
|
||||
const isNpxExecution = __dirname.includes('_npx') || __dirname.includes('.npm');
|
||||
|
||||
// If running via npx, we need to handle things differently
|
||||
if (isNpxExecution) {
|
||||
const arguments_ = process.argv.slice(2);
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Use the installer for all commands
|
||||
const bmadScriptPath = path.join(__dirname, 'installer', 'bin', 'bmad.js');
|
||||
@@ -26,9 +26,9 @@ if (isNpxExecution) {
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(`node "${bmadScriptPath}" ${arguments_.join(' ')}`, {
|
||||
execSync(`node "${bmadScriptPath}" ${args.join(' ')}`, {
|
||||
stdio: 'inherit',
|
||||
cwd: path.dirname(__dirname),
|
||||
cwd: path.dirname(__dirname)
|
||||
});
|
||||
} catch (error) {
|
||||
process.exit(error.status || 1);
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
const fs = require('node:fs').promises;
|
||||
const path = require('node:path');
|
||||
const DependencyResolver = require('../lib/dependency-resolver');
|
||||
const yamlUtilities = require('../lib/yaml-utils');
|
||||
const fs = require("node:fs").promises;
|
||||
const path = require("node:path");
|
||||
const DependencyResolver = require("../lib/dependency-resolver");
|
||||
const yamlUtils = require("../lib/yaml-utils");
|
||||
|
||||
class WebBuilder {
|
||||
constructor(options = {}) {
|
||||
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.templatePath = path.join(
|
||||
this.rootDir,
|
||||
'tools',
|
||||
'md-assets',
|
||||
'web-agent-startup-instructions.md',
|
||||
"tools",
|
||||
"md-assets",
|
||||
"web-agent-startup-instructions.md"
|
||||
);
|
||||
}
|
||||
|
||||
parseYaml(content) {
|
||||
const yaml = require('js-yaml');
|
||||
const yaml = require("js-yaml");
|
||||
return yaml.load(content);
|
||||
}
|
||||
|
||||
@@ -42,21 +42,11 @@ class WebBuilder {
|
||||
generateWebInstructions(bundleType, packName = null) {
|
||||
// Generate dynamic web instructions based on bundle type
|
||||
const rootExample = packName ? `.${packName}` : '.bmad-core';
|
||||
const examplePath = packName
|
||||
? `.${packName}/folder/filename.md`
|
||||
: '.bmad-core/folder/filename.md';
|
||||
const personasExample = packName
|
||||
? `.${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';
|
||||
const examplePath = packName ? `.${packName}/folder/filename.md` : '.bmad-core/folder/filename.md';
|
||||
const personasExample = packName ? `.${packName}/personas/analyst.md` : '.bmad-core/personas/analyst.md';
|
||||
const tasksExample = packName ? `.${packName}/tasks/create-story.md` : '.bmad-core/tasks/create-story.md';
|
||||
const utilsExample = packName ? `.${packName}/utils/template-format.md` : '.bmad-core/utils/template-format.md';
|
||||
const tasksRef = packName ? `.${packName}/tasks/create-story.md` : '.bmad-core/tasks/create-story.md';
|
||||
|
||||
return `# Web Agent Bundle Instructions
|
||||
|
||||
@@ -89,8 +79,8 @@ dependencies:
|
||||
|
||||
These references map directly to bundle sections:
|
||||
|
||||
- \`utils: template-format\` → Look for \`==================== START: ${utilitiesExample} ====================\`
|
||||
- \`tasks: create-story\` → Look for \`==================== START: ${tasksReference} ====================\`
|
||||
- \`utils: template-format\` → Look for \`==================== START: ${utilsExample} ====================\`
|
||||
- \`tasks: create-story\` → Look for \`==================== START: ${tasksRef} ====================\`
|
||||
|
||||
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.
|
||||
|
||||
@@ -122,10 +112,10 @@ These references map directly to bundle sections:
|
||||
|
||||
// Write to all output directories
|
||||
for (const outputDir of this.outputDirs) {
|
||||
const outputPath = path.join(outputDir, 'agents');
|
||||
const outputPath = path.join(outputDir, "agents");
|
||||
await fs.mkdir(outputPath, { recursive: true });
|
||||
const outputFile = path.join(outputPath, `${agentId}.txt`);
|
||||
await fs.writeFile(outputFile, bundle, 'utf8');
|
||||
await fs.writeFile(outputFile, bundle, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,10 +131,10 @@ These references map directly to bundle sections:
|
||||
|
||||
// Write to all output directories
|
||||
for (const outputDir of this.outputDirs) {
|
||||
const outputPath = path.join(outputDir, 'teams');
|
||||
const outputPath = path.join(outputDir, "teams");
|
||||
await fs.mkdir(outputPath, { recursive: true });
|
||||
const outputFile = path.join(outputPath, `${teamId}.txt`);
|
||||
await fs.writeFile(outputFile, bundle, 'utf8');
|
||||
await fs.writeFile(outputFile, bundle, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +157,7 @@ These references map directly to bundle sections:
|
||||
sections.push(this.formatSection(resourcePath, resource.content, 'bmad-core'));
|
||||
}
|
||||
|
||||
return sections.join('\n');
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
async buildTeamBundle(teamId) {
|
||||
@@ -192,12 +182,12 @@ These references map directly to bundle sections:
|
||||
sections.push(this.formatSection(resourcePath, resource.content, 'bmad-core'));
|
||||
}
|
||||
|
||||
return sections.join('\n');
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
processAgentContent(content) {
|
||||
// First, replace content before YAML with the template
|
||||
const yamlContent = yamlUtilities.extractYamlFromAgent(content);
|
||||
const yamlContent = yamlUtils.extractYamlFromAgent(content);
|
||||
if (!yamlContent) return content;
|
||||
|
||||
const yamlMatch = content.match(/```ya?ml\n([\s\S]*?)\n```/);
|
||||
@@ -208,24 +198,24 @@ These references map directly to bundle sections:
|
||||
|
||||
// Parse YAML and remove root and IDE-FILE-RESOLUTION properties
|
||||
try {
|
||||
const yaml = require('js-yaml');
|
||||
const yaml = require("js-yaml");
|
||||
const parsed = yaml.load(yamlContent);
|
||||
|
||||
// Remove the properties if they exist at root level
|
||||
delete parsed.root;
|
||||
delete parsed['IDE-FILE-RESOLUTION'];
|
||||
delete parsed['REQUEST-RESOLUTION'];
|
||||
delete parsed["IDE-FILE-RESOLUTION"];
|
||||
delete parsed["REQUEST-RESOLUTION"];
|
||||
|
||||
// Also remove from activation-instructions if they exist
|
||||
if (parsed['activation-instructions'] && Array.isArray(parsed['activation-instructions'])) {
|
||||
parsed['activation-instructions'] = parsed['activation-instructions'].filter(
|
||||
if (parsed["activation-instructions"] && Array.isArray(parsed["activation-instructions"])) {
|
||||
parsed["activation-instructions"] = parsed["activation-instructions"].filter(
|
||||
(instruction) => {
|
||||
return (
|
||||
typeof instruction === 'string' &&
|
||||
!instruction.startsWith('IDE-FILE-RESOLUTION:') &&
|
||||
!instruction.startsWith('REQUEST-RESOLUTION:')
|
||||
!instruction.startsWith("IDE-FILE-RESOLUTION:") &&
|
||||
!instruction.startsWith("REQUEST-RESOLUTION:")
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -233,25 +223,25 @@ These references map directly to bundle sections:
|
||||
const cleanedYaml = yaml.dump(parsed, { lineWidth: -1 });
|
||||
|
||||
// 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
|
||||
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.slice(Math.max(0, yamlEndIndex));
|
||||
const afterYaml = content.substring(yamlEndIndex);
|
||||
|
||||
return newHeader + '```yaml\n' + cleanedYaml.trim() + '\n```' + afterYaml;
|
||||
return newHeader + "```yaml\n" + cleanedYaml.trim() + "\n```" + afterYaml;
|
||||
} 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
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
formatSection(path, content, bundleRoot = 'bmad-core') {
|
||||
const separator = '====================';
|
||||
const separator = "====================";
|
||||
|
||||
// Process agent content if this is an agent file
|
||||
if (path.includes('/agents/')) {
|
||||
if (path.includes("/agents/")) {
|
||||
content = this.processAgentContent(content);
|
||||
}
|
||||
|
||||
@@ -262,17 +252,17 @@ These references map directly to bundle sections:
|
||||
`${separator} START: ${path} ${separator}`,
|
||||
content.trim(),
|
||||
`${separator} END: ${path} ${separator}`,
|
||||
'',
|
||||
].join('\n');
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
replaceRootReferences(content, bundleRoot) {
|
||||
// Replace {root} with the appropriate bundle root path
|
||||
return content.replaceAll('{root}', `.${bundleRoot}`);
|
||||
return content.replace(/\{root\}/g, `.${bundleRoot}`);
|
||||
}
|
||||
|
||||
async validate() {
|
||||
console.log('Validating agent configurations...');
|
||||
console.log("Validating agent configurations...");
|
||||
const agents = await this.resolver.listAgents();
|
||||
for (const agentId of agents) {
|
||||
try {
|
||||
@@ -284,7 +274,7 @@ These references map directly to bundle sections:
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nValidating team configurations...');
|
||||
console.log("\nValidating team configurations...");
|
||||
const teams = await this.resolver.listTeams();
|
||||
for (const teamId of teams) {
|
||||
try {
|
||||
@@ -309,54 +299,54 @@ These references map directly to bundle sections:
|
||||
}
|
||||
|
||||
async buildExpansionPack(packName, options = {}) {
|
||||
const packDir = path.join(this.rootDir, 'expansion-packs', packName);
|
||||
const outputDirectories = [path.join(this.rootDir, 'dist', 'expansion-packs', packName)];
|
||||
const packDir = path.join(this.rootDir, "expansion-packs", packName);
|
||||
const outputDirs = [path.join(this.rootDir, "dist", "expansion-packs", packName)];
|
||||
|
||||
// Clean output directories if requested
|
||||
if (options.clean !== false) {
|
||||
for (const outputDir of outputDirectories) {
|
||||
for (const outputDir of outputDirs) {
|
||||
try {
|
||||
await fs.rm(outputDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Directory might not exist, that's fine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build individual agents first
|
||||
const agentsDir = path.join(packDir, 'agents');
|
||||
const agentsDir = path.join(packDir, "agents");
|
||||
try {
|
||||
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) {
|
||||
console.log(` Building individual agents for ${packName}:`);
|
||||
|
||||
for (const agentFile of agentMarkdownFiles) {
|
||||
const agentName = agentFile.replace('.md', '');
|
||||
const agentName = agentFile.replace(".md", "");
|
||||
console.log(` - ${agentName}`);
|
||||
|
||||
// Build individual agent bundle
|
||||
const bundle = await this.buildExpansionAgentBundle(packName, packDir, agentName);
|
||||
|
||||
// Write to all output directories
|
||||
for (const outputDir of outputDirectories) {
|
||||
const agentsOutputDir = path.join(outputDir, 'agents');
|
||||
for (const outputDir of outputDirs) {
|
||||
const agentsOutputDir = path.join(outputDir, "agents");
|
||||
await fs.mkdir(agentsOutputDir, { recursive: true });
|
||||
const outputFile = path.join(agentsOutputDir, `${agentName}.txt`);
|
||||
await fs.writeFile(outputFile, bundle, 'utf8');
|
||||
await fs.writeFile(outputFile, bundle, "utf8");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.debug(` No agents directory found for ${packName}`);
|
||||
}
|
||||
|
||||
// Build team bundle
|
||||
const agentTeamsDir = path.join(packDir, 'agent-teams');
|
||||
const agentTeamsDir = path.join(packDir, "agent-teams");
|
||||
try {
|
||||
const teamFiles = await fs.readdir(agentTeamsDir);
|
||||
const teamFile = teamFiles.find((f) => f.endsWith('.yaml'));
|
||||
const teamFile = teamFiles.find((f) => f.endsWith(".yaml"));
|
||||
|
||||
if (teamFile) {
|
||||
console.log(` Building team bundle for ${packName}`);
|
||||
@@ -366,17 +356,17 @@ These references map directly to bundle sections:
|
||||
const bundle = await this.buildExpansionTeamBundle(packName, packDir, teamConfigPath);
|
||||
|
||||
// Write to all output directories
|
||||
for (const outputDir of outputDirectories) {
|
||||
const teamsOutputDir = path.join(outputDir, 'teams');
|
||||
for (const outputDir of outputDirs) {
|
||||
const teamsOutputDir = path.join(outputDir, "teams");
|
||||
await fs.mkdir(teamsOutputDir, { recursive: true });
|
||||
const outputFile = path.join(teamsOutputDir, teamFile.replace('.yaml', '.txt'));
|
||||
await fs.writeFile(outputFile, bundle, 'utf8');
|
||||
const outputFile = path.join(teamsOutputDir, teamFile.replace(".yaml", ".txt"));
|
||||
await fs.writeFile(outputFile, bundle, "utf8");
|
||||
console.log(` ✓ Created bundle: ${path.relative(this.rootDir, outputFile)}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(` ⚠ No team configuration found in ${packName}/agent-teams/`);
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.warn(` ⚠ No agent-teams directory found for ${packName}`);
|
||||
}
|
||||
}
|
||||
@@ -386,16 +376,16 @@ These references map directly to bundle sections:
|
||||
const sections = [template];
|
||||
|
||||
// Add agent configuration
|
||||
const agentPath = path.join(packDir, 'agents', `${agentName}.md`);
|
||||
const agentContent = await fs.readFile(agentPath, 'utf8');
|
||||
const agentPath = path.join(packDir, "agents", `${agentName}.md`);
|
||||
const agentContent = await fs.readFile(agentPath, "utf8");
|
||||
const agentWebPath = this.convertToWebPath(agentPath, packName);
|
||||
sections.push(this.formatSection(agentWebPath, agentContent, packName));
|
||||
|
||||
// Resolve and add agent dependencies
|
||||
const yamlContent = yamlUtilities.extractYamlFromAgent(agentContent);
|
||||
const yamlContent = yamlUtils.extractYamlFromAgent(agentContent);
|
||||
if (yamlContent) {
|
||||
try {
|
||||
const yaml = require('js-yaml');
|
||||
const yaml = require("js-yaml");
|
||||
const agentConfig = yaml.load(yamlContent);
|
||||
|
||||
if (agentConfig.dependencies) {
|
||||
@@ -408,43 +398,59 @@ These references map directly to bundle sections:
|
||||
// Try expansion pack first
|
||||
const resourcePath = path.join(packDir, resourceType, resourceName);
|
||||
try {
|
||||
const resourceContent = await fs.readFile(resourcePath, 'utf8');
|
||||
const resourceContent = await fs.readFile(resourcePath, "utf8");
|
||||
const resourceWebPath = this.convertToWebPath(resourcePath, packName);
|
||||
sections.push(this.formatSection(resourceWebPath, resourceContent, packName));
|
||||
sections.push(
|
||||
this.formatSection(resourceWebPath, resourceContent, packName)
|
||||
);
|
||||
found = true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Not in expansion pack, continue
|
||||
}
|
||||
|
||||
// If not found in expansion pack, try core
|
||||
if (!found) {
|
||||
const corePath = path.join(this.rootDir, 'bmad-core', resourceType, resourceName);
|
||||
const corePath = path.join(
|
||||
this.rootDir,
|
||||
"bmad-core",
|
||||
resourceType,
|
||||
resourceName
|
||||
);
|
||||
try {
|
||||
const coreContent = await fs.readFile(corePath, 'utf8');
|
||||
const coreContent = await fs.readFile(corePath, "utf8");
|
||||
const coreWebPath = this.convertToWebPath(corePath, packName);
|
||||
sections.push(this.formatSection(coreWebPath, coreContent, packName));
|
||||
sections.push(
|
||||
this.formatSection(coreWebPath, coreContent, packName)
|
||||
);
|
||||
found = true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Not in core either, continue
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in core, try common folder
|
||||
if (!found) {
|
||||
const commonPath = path.join(this.rootDir, 'common', resourceType, resourceName);
|
||||
const commonPath = path.join(
|
||||
this.rootDir,
|
||||
"common",
|
||||
resourceType,
|
||||
resourceName
|
||||
);
|
||||
try {
|
||||
const commonContent = await fs.readFile(commonPath, 'utf8');
|
||||
const commonContent = await fs.readFile(commonPath, "utf8");
|
||||
const commonWebPath = this.convertToWebPath(commonPath, packName);
|
||||
sections.push(this.formatSection(commonWebPath, commonContent, packName));
|
||||
sections.push(
|
||||
this.formatSection(commonWebPath, commonContent, packName)
|
||||
);
|
||||
found = true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Not in common either, continue
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
console.warn(
|
||||
` ⚠ Dependency ${resourceType}#${resourceName} not found in expansion pack or core`,
|
||||
` ⚠ Dependency ${resourceType}#${resourceName} not found in expansion pack or core`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -456,7 +462,7 @@ These references map directly to bundle sections:
|
||||
}
|
||||
}
|
||||
|
||||
return sections.join('\n');
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
async buildExpansionTeamBundle(packName, packDir, teamConfigPath) {
|
||||
@@ -465,38 +471,38 @@ These references map directly to bundle sections:
|
||||
const sections = [template];
|
||||
|
||||
// Add team configuration and parse to get agent list
|
||||
const teamContent = await fs.readFile(teamConfigPath, 'utf8');
|
||||
const teamFileName = path.basename(teamConfigPath, '.yaml');
|
||||
const teamContent = await fs.readFile(teamConfigPath, "utf8");
|
||||
const teamFileName = path.basename(teamConfigPath, ".yaml");
|
||||
const teamConfig = this.parseYaml(teamContent);
|
||||
const teamWebPath = this.convertToWebPath(teamConfigPath, packName);
|
||||
sections.push(this.formatSection(teamWebPath, teamContent, packName));
|
||||
|
||||
// Get list of expansion pack agents
|
||||
const expansionAgents = new Set();
|
||||
const agentsDir = path.join(packDir, 'agents');
|
||||
const agentsDir = path.join(packDir, "agents");
|
||||
try {
|
||||
const agentFiles = await fs.readdir(agentsDir);
|
||||
for (const agentFile of agentFiles.filter((f) => f.endsWith('.md'))) {
|
||||
const agentName = agentFile.replace('.md', '');
|
||||
for (const agentFile of agentFiles.filter((f) => f.endsWith(".md"))) {
|
||||
const agentName = agentFile.replace(".md", "");
|
||||
expansionAgents.add(agentName);
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.warn(` ⚠ No agents directory found in ${packName}`);
|
||||
}
|
||||
|
||||
// Build a map of all available expansion pack resources for override checking
|
||||
const expansionResources = new Map();
|
||||
const resourceDirectories = ['templates', 'tasks', 'checklists', 'workflows', 'data'];
|
||||
for (const resourceDir of resourceDirectories) {
|
||||
const resourceDirs = ["templates", "tasks", "checklists", "workflows", "data"];
|
||||
for (const resourceDir of resourceDirs) {
|
||||
const resourcePath = path.join(packDir, resourceDir);
|
||||
try {
|
||||
const resourceFiles = await fs.readdir(resourcePath);
|
||||
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);
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Directory might not exist, that's fine
|
||||
}
|
||||
}
|
||||
@@ -505,9 +511,9 @@ These references map directly to bundle sections:
|
||||
const agentsToProcess = teamConfig.agents || [];
|
||||
|
||||
// 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`);
|
||||
agentsToProcess.unshift('bmad-orchestrator');
|
||||
agentsToProcess.unshift("bmad-orchestrator");
|
||||
}
|
||||
|
||||
// Track all dependencies from all agents (deduplicated)
|
||||
@@ -517,7 +523,7 @@ These references map directly to bundle sections:
|
||||
if (expansionAgents.has(agentId)) {
|
||||
// Use expansion pack version (override)
|
||||
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);
|
||||
sections.push(this.formatSection(expansionAgentWebPath, agentContent, packName));
|
||||
|
||||
@@ -545,13 +551,13 @@ These references map directly to bundle sections:
|
||||
} else {
|
||||
// Use core BMad version
|
||||
try {
|
||||
const coreAgentPath = path.join(this.rootDir, 'bmad-core', 'agents', `${agentId}.md`);
|
||||
const coreAgentContent = await fs.readFile(coreAgentPath, 'utf8');
|
||||
const coreAgentPath = path.join(this.rootDir, "bmad-core", "agents", `${agentId}.md`);
|
||||
const coreAgentContent = await fs.readFile(coreAgentPath, "utf8");
|
||||
const coreAgentWebPath = this.convertToWebPath(coreAgentPath, packName);
|
||||
sections.push(this.formatSection(coreAgentWebPath, coreAgentContent, packName));
|
||||
|
||||
// Parse and collect dependencies from core agent
|
||||
const yamlContent = yamlUtilities.extractYamlFromAgent(coreAgentContent, true);
|
||||
const yamlContent = yamlUtils.extractYamlFromAgent(coreAgentContent, true);
|
||||
if (yamlContent) {
|
||||
try {
|
||||
const agentConfig = this.parseYaml(yamlContent);
|
||||
@@ -571,7 +577,7 @@ These references map directly to bundle sections:
|
||||
console.debug(`Failed to parse agent YAML for ${agentId}:`, error.message);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.warn(` ⚠ Agent ${agentId} not found in core or expansion pack`);
|
||||
}
|
||||
}
|
||||
@@ -587,38 +593,38 @@ These references map directly to bundle sections:
|
||||
// We know it exists in expansion pack, find and load it
|
||||
const expansionPath = path.join(packDir, dep.type, dep.name);
|
||||
try {
|
||||
const content = await fs.readFile(expansionPath, 'utf8');
|
||||
const content = await fs.readFile(expansionPath, "utf8");
|
||||
const expansionWebPath = this.convertToWebPath(expansionPath, packName);
|
||||
sections.push(this.formatSection(expansionWebPath, content, packName));
|
||||
console.log(` ✓ Using expansion override for ${key}`);
|
||||
found = true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Try next extension
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in expansion pack (or doesn't exist there), try core
|
||||
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 {
|
||||
const content = await fs.readFile(corePath, 'utf8');
|
||||
const content = await fs.readFile(corePath, "utf8");
|
||||
const coreWebPath = this.convertToWebPath(corePath, packName);
|
||||
sections.push(this.formatSection(coreWebPath, content, packName));
|
||||
found = true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Not in core either, continue
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in core, try common folder
|
||||
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 {
|
||||
const content = await fs.readFile(commonPath, 'utf8');
|
||||
const content = await fs.readFile(commonPath, "utf8");
|
||||
const commonWebPath = this.convertToWebPath(commonPath, packName);
|
||||
sections.push(this.formatSection(commonWebPath, content, packName));
|
||||
found = true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Not in common either, continue
|
||||
}
|
||||
}
|
||||
@@ -629,16 +635,16 @@ These references map directly to bundle sections:
|
||||
}
|
||||
|
||||
// Add remaining expansion pack resources not already included as dependencies
|
||||
for (const resourceDir of resourceDirectories) {
|
||||
for (const resourceDir of resourceDirs) {
|
||||
const resourcePath = path.join(packDir, resourceDir);
|
||||
try {
|
||||
const resourceFiles = await fs.readdir(resourcePath);
|
||||
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 fileContent = await fs.readFile(filePath, 'utf8');
|
||||
const fileName = resourceFile.replace(/\.(md|yaml)$/, '');
|
||||
const fileContent = await fs.readFile(filePath, "utf8");
|
||||
const fileName = resourceFile.replace(/\.(md|yaml)$/, "");
|
||||
|
||||
// Only add if not already included as a dependency
|
||||
const resourceKey = `${resourceDir}#${fileName}`;
|
||||
@@ -648,21 +654,21 @@ These references map directly to bundle sections:
|
||||
sections.push(this.formatSection(resourceWebPath, fileContent, packName));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Directory might not exist, that's fine
|
||||
}
|
||||
}
|
||||
|
||||
return sections.join('\n');
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
async listExpansionPacks() {
|
||||
const expansionPacksDir = path.join(this.rootDir, 'expansion-packs');
|
||||
const expansionPacksDir = path.join(this.rootDir, "expansion-packs");
|
||||
try {
|
||||
const entries = await fs.readdir(expansionPacksDir, { withFileTypes: true });
|
||||
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
||||
} catch {
|
||||
console.warn('No expansion-packs directory found');
|
||||
} catch (error) {
|
||||
console.warn("No expansion-packs directory found");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const arguments_ = process.argv.slice(2);
|
||||
const bumpType = arguments_[0] || 'minor'; // default to minor
|
||||
const args = process.argv.slice(2);
|
||||
const bumpType = args[0] || 'minor'; // default to minor
|
||||
|
||||
if (!['major', 'minor', 'patch'].includes(bumpType)) {
|
||||
console.log('Usage: node bump-all-versions.js [major|minor|patch]');
|
||||
@@ -15,18 +17,14 @@ function bumpVersion(currentVersion, type) {
|
||||
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||
|
||||
switch (type) {
|
||||
case 'major': {
|
||||
case 'major':
|
||||
return `${major + 1}.0.0`;
|
||||
}
|
||||
case 'minor': {
|
||||
case 'minor':
|
||||
return `${major}.${minor + 1}.0`;
|
||||
}
|
||||
case 'patch': {
|
||||
case 'patch':
|
||||
return `${major}.${minor}.${patch + 1}`;
|
||||
}
|
||||
default: {
|
||||
default:
|
||||
return currentVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +43,7 @@ async function bumpAllVersions() {
|
||||
|
||||
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}`);
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to update BMad Core: ${error.message}`);
|
||||
@@ -81,6 +74,7 @@ async function bumpAllVersions() {
|
||||
|
||||
updatedItems.push({ type: 'expansion', name: packId, oldVersion, newVersion });
|
||||
console.log(`✓ ${packId}: ${oldVersion} → ${newVersion}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to update ${packId}: ${error.message}`);
|
||||
}
|
||||
@@ -89,23 +83,20 @@ async function bumpAllVersions() {
|
||||
}
|
||||
|
||||
if (updatedItems.length > 0) {
|
||||
const coreCount = updatedItems.filter((index) => index.type === 'core').length;
|
||||
const expansionCount = updatedItems.filter((index) => index.type === 'expansion').length;
|
||||
const coreCount = updatedItems.filter(i => i.type === 'core').length;
|
||||
const expansionCount = updatedItems.filter(i => i.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 (expansionCount > 0) console.log(` - ${expansionCount} expansion pack(s)`);
|
||||
|
||||
console.log('\nNext steps:');
|
||||
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 {
|
||||
console.log('No items found to update');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reading expansion packs directory:', error.message);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Load required modules
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
// Parse CLI arguments
|
||||
const arguments_ = process.argv.slice(2);
|
||||
const packId = arguments_[0];
|
||||
const bumpType = arguments_[1] || 'minor';
|
||||
const args = process.argv.slice(2);
|
||||
const packId = args[0];
|
||||
const bumpType = args[1] || 'minor';
|
||||
|
||||
// Validate arguments
|
||||
if (!packId || arguments_.length > 2) {
|
||||
if (!packId || args.length > 2) {
|
||||
console.log('Usage: node bump-expansion-version.js <expansion-pack-id> [major|minor|patch]');
|
||||
console.log('Default: minor');
|
||||
console.log('Example: node bump-expansion-version.js bmad-creator-tools patch');
|
||||
@@ -26,18 +28,10 @@ function bumpVersion(currentVersion, type) {
|
||||
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||
|
||||
switch (type) {
|
||||
case 'major': {
|
||||
return `${major + 1}.0.0`;
|
||||
}
|
||||
case 'minor': {
|
||||
return `${major}.${minor + 1}.0`;
|
||||
}
|
||||
case 'patch': {
|
||||
return `${major}.${minor}.${patch + 1}`;
|
||||
}
|
||||
default: {
|
||||
return currentVersion;
|
||||
}
|
||||
case 'major': return `${major + 1}.0.0`;
|
||||
case 'minor': return `${major}.${minor + 1}.0`;
|
||||
case 'patch': return `${major}.${minor}.${patch + 1}`;
|
||||
default: return currentVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +47,11 @@ async function updateVersion() {
|
||||
const packsDir = path.join(__dirname, '..', 'expansion-packs');
|
||||
const entries = fs.readdirSync(packsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
||||
console.log(` - ${entry.name}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -78,9 +72,8 @@ async function updateVersion() {
|
||||
console.log(`\n✓ Successfully bumped ${packId} with ${bumpType} version bump`);
|
||||
console.log('\nNext steps:');
|
||||
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) {
|
||||
console.error('Error updating version:', error.message);
|
||||
process.exit(1);
|
||||
|
||||
14
tools/cli.js
14
tools/cli.js
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { Command } = require('commander');
|
||||
const WebBuilder = require('./builders/web-builder');
|
||||
const V3ToV4Upgrader = require('./upgraders/v3-to-v4-upgrader');
|
||||
const IdeSetup = require('./installer/lib/ide-setup');
|
||||
const path = require('node:path');
|
||||
const path = require('path');
|
||||
|
||||
const program = new Command();
|
||||
|
||||
@@ -21,7 +23,7 @@ program
|
||||
.option('--no-clean', 'Skip cleaning output directories')
|
||||
.action(async (options) => {
|
||||
const builder = new WebBuilder({
|
||||
rootDir: process.cwd(),
|
||||
rootDir: process.cwd()
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -64,7 +66,7 @@ program
|
||||
.option('--no-clean', 'Skip cleaning output directories')
|
||||
.action(async (options) => {
|
||||
const builder = new WebBuilder({
|
||||
rootDir: process.cwd(),
|
||||
rootDir: process.cwd()
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -90,7 +92,7 @@ program
|
||||
const builder = new WebBuilder({ rootDir: process.cwd() });
|
||||
const agents = await builder.resolver.listAgents();
|
||||
console.log('Available agents:');
|
||||
for (const agent of agents) console.log(` - ${agent}`);
|
||||
agents.forEach(agent => console.log(` - ${agent}`));
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -101,7 +103,7 @@ program
|
||||
const builder = new WebBuilder({ rootDir: process.cwd() });
|
||||
const expansions = await builder.listExpansionPacks();
|
||||
console.log('Available expansion packs:');
|
||||
for (const expansion of expansions) console.log(` - ${expansion}`);
|
||||
expansions.forEach(expansion => console.log(` - ${expansion}`));
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -145,7 +147,7 @@ program
|
||||
await upgrader.upgrade({
|
||||
projectPath: options.project,
|
||||
dryRun: options.dryRun,
|
||||
backup: options.backup,
|
||||
backup: options.backup
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { isBinaryFile } = require('./binary.js');
|
||||
const fs = require("fs-extra");
|
||||
const path = require("node:path");
|
||||
const os = require("node:os");
|
||||
const { isBinaryFile } = require("./binary.js");
|
||||
|
||||
/**
|
||||
* 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.
|
||||
// - Base on 2x logical CPUs, clamped to [2, 64]
|
||||
// - 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));
|
||||
if (files.length > 0 && files.length < concurrency) {
|
||||
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);
|
||||
if (binary) {
|
||||
const { size } = await fs.stat(filePath);
|
||||
const size = (await fs.stat(filePath)).size;
|
||||
results.binaryFiles.push({ path: relativePath, absolutePath: filePath, size });
|
||||
} else {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const content = await fs.readFile(filePath, "utf8");
|
||||
results.textFiles.push({
|
||||
path: relativePath,
|
||||
absolutePath: filePath,
|
||||
content,
|
||||
size: content.length,
|
||||
lines: content.split('\n').length,
|
||||
lines: content.split("\n").length,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -63,8 +63,8 @@ async function aggregateFileContents(files, rootDir, spinner = null) {
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < files.length; index += concurrency) {
|
||||
const slice = files.slice(index, index + concurrency);
|
||||
for (let i = 0; i < files.length; i += concurrency) {
|
||||
const slice = files.slice(i, i + concurrency);
|
||||
await Promise.all(slice.map(processOne));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const fsp = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const { Buffer } = require('node:buffer');
|
||||
const fsp = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { Buffer } = require("node:buffer");
|
||||
|
||||
/**
|
||||
* Efficiently determine if a file is binary without reading the whole file.
|
||||
@@ -13,54 +13,25 @@ async function isBinaryFile(filePath) {
|
||||
try {
|
||||
const stats = await fsp.stat(filePath);
|
||||
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([
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.png',
|
||||
'.gif',
|
||||
'.bmp',
|
||||
'.ico',
|
||||
'.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',
|
||||
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".ico", ".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 extension = path.extname(filePath).toLowerCase();
|
||||
if (binaryExtensions.has(extension)) return true;
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (binaryExtensions.has(ext)) return true;
|
||||
if (stats.size === 0) return false;
|
||||
|
||||
const sampleSize = Math.min(4096, stats.size);
|
||||
const fd = await fsp.open(filePath, 'r');
|
||||
const fd = await fsp.open(filePath, "r");
|
||||
try {
|
||||
const buffer = Buffer.allocUnsafe(sampleSize);
|
||||
const { bytesRead } = await fd.read(buffer, 0, sampleSize, 0);
|
||||
@@ -70,7 +41,9 @@ async function isBinaryFile(filePath) {
|
||||
await fd.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not determine if file is binary: ${filePath} - ${error.message}`);
|
||||
console.warn(
|
||||
`Warning: Could not determine if file is binary: ${filePath} - ${error.message}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
const path = require('node:path');
|
||||
const { execFile } = require('node:child_process');
|
||||
const { promisify } = require('node:util');
|
||||
const { glob } = require('glob');
|
||||
const { loadIgnore } = require('./ignoreRules.js');
|
||||
const path = require("node:path");
|
||||
const { execFile } = require("node:child_process");
|
||||
const { promisify } = require("node:util");
|
||||
const { glob } = require("glob");
|
||||
const { loadIgnore } = require("./ignoreRules.js");
|
||||
|
||||
const pExecFile = promisify(execFile);
|
||||
|
||||
async function isGitRepo(rootDir) {
|
||||
try {
|
||||
const { stdout } = await pExecFile('git', ['rev-parse', '--is-inside-work-tree'], {
|
||||
cwd: rootDir,
|
||||
});
|
||||
return (
|
||||
String(stdout || '')
|
||||
.toString()
|
||||
.trim() === 'true'
|
||||
);
|
||||
const { stdout } = await pExecFile("git", [
|
||||
"rev-parse",
|
||||
"--is-inside-work-tree",
|
||||
], { cwd: rootDir });
|
||||
return String(stdout || "").toString().trim() === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -23,10 +20,12 @@ async function isGitRepo(rootDir) {
|
||||
|
||||
async function gitListFiles(rootDir) {
|
||||
try {
|
||||
const { stdout } = await pExecFile('git', ['ls-files', '-co', '--exclude-standard'], {
|
||||
cwd: rootDir,
|
||||
});
|
||||
return String(stdout || '')
|
||||
const { stdout } = await pExecFile("git", [
|
||||
"ls-files",
|
||||
"-co",
|
||||
"--exclude-standard",
|
||||
], { cwd: rootDir });
|
||||
return String(stdout || "")
|
||||
.split(/\r?\n/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
@@ -49,14 +48,14 @@ async function discoverFiles(rootDir, options = {}) {
|
||||
const { filter } = await loadIgnore(rootDir);
|
||||
|
||||
// Try git first
|
||||
if (preferGit && (await isGitRepo(rootDir))) {
|
||||
if (preferGit && await isGitRepo(rootDir)) {
|
||||
const relFiles = await gitListFiles(rootDir);
|
||||
const filteredRel = relFiles.filter((p) => filter(p));
|
||||
return filteredRel.map((p) => path.resolve(rootDir, p));
|
||||
}
|
||||
|
||||
// Glob fallback
|
||||
const globbed = await glob('**/*', {
|
||||
const globbed = await glob("**/*", {
|
||||
cwd: rootDir,
|
||||
nodir: true,
|
||||
dot: true,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const path = require('node:path');
|
||||
const discovery = require('./discovery.js');
|
||||
const ignoreRules = require('./ignoreRules.js');
|
||||
const { isBinaryFile } = require('./binary.js');
|
||||
const { aggregateFileContents } = require('./aggregate.js');
|
||||
const path = require("node:path");
|
||||
const discovery = require("./discovery.js");
|
||||
const ignoreRules = require("./ignoreRules.js");
|
||||
const { isBinaryFile } = require("./binary.js");
|
||||
const { aggregateFileContents } = require("./aggregate.js");
|
||||
|
||||
// Backward-compatible signature; delegate to central loader
|
||||
async function parseGitignore(gitignorePath) {
|
||||
@@ -14,7 +14,7 @@ async function discoverFiles(rootDir) {
|
||||
// Delegate to discovery module which respects .gitignore and defaults
|
||||
return await discovery.discoverFiles(rootDir, { preferGit: true });
|
||||
} catch (error) {
|
||||
console.error('Error discovering files:', error.message);
|
||||
console.error("Error discovering files:", error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,147 +1,147 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const ignore = require('ignore');
|
||||
const fs = require("fs-extra");
|
||||
const path = require("node:path");
|
||||
const ignore = require("ignore");
|
||||
|
||||
// Central default ignore patterns for discovery and filtering.
|
||||
// These complement .gitignore and are applied regardless of VCS presence.
|
||||
const DEFAULT_PATTERNS = [
|
||||
// Project/VCS
|
||||
'**/.bmad-core/**',
|
||||
'**/.git/**',
|
||||
'**/.svn/**',
|
||||
'**/.hg/**',
|
||||
'**/.bzr/**',
|
||||
"**/.bmad-core/**",
|
||||
"**/.git/**",
|
||||
"**/.svn/**",
|
||||
"**/.hg/**",
|
||||
"**/.bzr/**",
|
||||
// Package/build outputs
|
||||
'**/node_modules/**',
|
||||
'**/bower_components/**',
|
||||
'**/vendor/**',
|
||||
'**/packages/**',
|
||||
'**/build/**',
|
||||
'**/dist/**',
|
||||
'**/out/**',
|
||||
'**/target/**',
|
||||
'**/bin/**',
|
||||
'**/obj/**',
|
||||
'**/release/**',
|
||||
'**/debug/**',
|
||||
"**/node_modules/**",
|
||||
"**/bower_components/**",
|
||||
"**/vendor/**",
|
||||
"**/packages/**",
|
||||
"**/build/**",
|
||||
"**/dist/**",
|
||||
"**/out/**",
|
||||
"**/target/**",
|
||||
"**/bin/**",
|
||||
"**/obj/**",
|
||||
"**/release/**",
|
||||
"**/debug/**",
|
||||
// Environments
|
||||
'**/.venv/**',
|
||||
'**/venv/**',
|
||||
'**/.virtualenv/**',
|
||||
'**/virtualenv/**',
|
||||
'**/env/**',
|
||||
"**/.venv/**",
|
||||
"**/venv/**",
|
||||
"**/.virtualenv/**",
|
||||
"**/virtualenv/**",
|
||||
"**/env/**",
|
||||
// Logs & coverage
|
||||
'**/*.log',
|
||||
'**/npm-debug.log*',
|
||||
'**/yarn-debug.log*',
|
||||
'**/yarn-error.log*',
|
||||
'**/lerna-debug.log*',
|
||||
'**/coverage/**',
|
||||
'**/.nyc_output/**',
|
||||
'**/.coverage/**',
|
||||
'**/test-results/**',
|
||||
"**/*.log",
|
||||
"**/npm-debug.log*",
|
||||
"**/yarn-debug.log*",
|
||||
"**/yarn-error.log*",
|
||||
"**/lerna-debug.log*",
|
||||
"**/coverage/**",
|
||||
"**/.nyc_output/**",
|
||||
"**/.coverage/**",
|
||||
"**/test-results/**",
|
||||
// Caches & temp
|
||||
'**/.cache/**',
|
||||
'**/.tmp/**',
|
||||
'**/.temp/**',
|
||||
'**/tmp/**',
|
||||
'**/temp/**',
|
||||
'**/.sass-cache/**',
|
||||
"**/.cache/**",
|
||||
"**/.tmp/**",
|
||||
"**/.temp/**",
|
||||
"**/tmp/**",
|
||||
"**/temp/**",
|
||||
"**/.sass-cache/**",
|
||||
// IDE/editor
|
||||
'**/.vscode/**',
|
||||
'**/.idea/**',
|
||||
'**/*.swp',
|
||||
'**/*.swo',
|
||||
'**/*~',
|
||||
'**/.project',
|
||||
'**/.classpath',
|
||||
'**/.settings/**',
|
||||
'**/*.sublime-project',
|
||||
'**/*.sublime-workspace',
|
||||
"**/.vscode/**",
|
||||
"**/.idea/**",
|
||||
"**/*.swp",
|
||||
"**/*.swo",
|
||||
"**/*~",
|
||||
"**/.project",
|
||||
"**/.classpath",
|
||||
"**/.settings/**",
|
||||
"**/*.sublime-project",
|
||||
"**/*.sublime-workspace",
|
||||
// Lockfiles
|
||||
'**/package-lock.json',
|
||||
'**/yarn.lock',
|
||||
'**/pnpm-lock.yaml',
|
||||
'**/composer.lock',
|
||||
'**/Pipfile.lock',
|
||||
"**/package-lock.json",
|
||||
"**/yarn.lock",
|
||||
"**/pnpm-lock.yaml",
|
||||
"**/composer.lock",
|
||||
"**/Pipfile.lock",
|
||||
// Python/Java/compiled artifacts
|
||||
'**/*.pyc',
|
||||
'**/*.pyo',
|
||||
'**/*.pyd',
|
||||
'**/__pycache__/**',
|
||||
'**/*.class',
|
||||
'**/*.jar',
|
||||
'**/*.war',
|
||||
'**/*.ear',
|
||||
'**/*.o',
|
||||
'**/*.so',
|
||||
'**/*.dll',
|
||||
'**/*.exe',
|
||||
"**/*.pyc",
|
||||
"**/*.pyo",
|
||||
"**/*.pyd",
|
||||
"**/__pycache__/**",
|
||||
"**/*.class",
|
||||
"**/*.jar",
|
||||
"**/*.war",
|
||||
"**/*.ear",
|
||||
"**/*.o",
|
||||
"**/*.so",
|
||||
"**/*.dll",
|
||||
"**/*.exe",
|
||||
// System junk
|
||||
'**/lib64/**',
|
||||
'**/.venv/lib64/**',
|
||||
'**/venv/lib64/**',
|
||||
'**/_site/**',
|
||||
'**/.jekyll-cache/**',
|
||||
'**/.jekyll-metadata',
|
||||
'**/.DS_Store',
|
||||
'**/.DS_Store?',
|
||||
'**/._*',
|
||||
'**/.Spotlight-V100/**',
|
||||
'**/.Trashes/**',
|
||||
'**/ehthumbs.db',
|
||||
'**/Thumbs.db',
|
||||
'**/desktop.ini',
|
||||
"**/lib64/**",
|
||||
"**/.venv/lib64/**",
|
||||
"**/venv/lib64/**",
|
||||
"**/_site/**",
|
||||
"**/.jekyll-cache/**",
|
||||
"**/.jekyll-metadata",
|
||||
"**/.DS_Store",
|
||||
"**/.DS_Store?",
|
||||
"**/._*",
|
||||
"**/.Spotlight-V100/**",
|
||||
"**/.Trashes/**",
|
||||
"**/ehthumbs.db",
|
||||
"**/Thumbs.db",
|
||||
"**/desktop.ini",
|
||||
// XML outputs
|
||||
'**/flattened-codebase.xml',
|
||||
'**/repomix-output.xml',
|
||||
"**/flattened-codebase.xml",
|
||||
"**/repomix-output.xml",
|
||||
// Images, media, fonts, archives, docs, dylibs
|
||||
'**/*.jpg',
|
||||
'**/*.jpeg',
|
||||
'**/*.png',
|
||||
'**/*.gif',
|
||||
'**/*.bmp',
|
||||
'**/*.ico',
|
||||
'**/*.svg',
|
||||
'**/*.pdf',
|
||||
'**/*.doc',
|
||||
'**/*.docx',
|
||||
'**/*.xls',
|
||||
'**/*.xlsx',
|
||||
'**/*.ppt',
|
||||
'**/*.pptx',
|
||||
'**/*.zip',
|
||||
'**/*.tar',
|
||||
'**/*.gz',
|
||||
'**/*.rar',
|
||||
'**/*.7z',
|
||||
'**/*.dylib',
|
||||
'**/*.mp3',
|
||||
'**/*.mp4',
|
||||
'**/*.avi',
|
||||
'**/*.mov',
|
||||
'**/*.wav',
|
||||
'**/*.ttf',
|
||||
'**/*.otf',
|
||||
'**/*.woff',
|
||||
'**/*.woff2',
|
||||
"**/*.jpg",
|
||||
"**/*.jpeg",
|
||||
"**/*.png",
|
||||
"**/*.gif",
|
||||
"**/*.bmp",
|
||||
"**/*.ico",
|
||||
"**/*.svg",
|
||||
"**/*.pdf",
|
||||
"**/*.doc",
|
||||
"**/*.docx",
|
||||
"**/*.xls",
|
||||
"**/*.xlsx",
|
||||
"**/*.ppt",
|
||||
"**/*.pptx",
|
||||
"**/*.zip",
|
||||
"**/*.tar",
|
||||
"**/*.gz",
|
||||
"**/*.rar",
|
||||
"**/*.7z",
|
||||
"**/*.dylib",
|
||||
"**/*.mp3",
|
||||
"**/*.mp4",
|
||||
"**/*.avi",
|
||||
"**/*.mov",
|
||||
"**/*.wav",
|
||||
"**/*.ttf",
|
||||
"**/*.otf",
|
||||
"**/*.woff",
|
||||
"**/*.woff2",
|
||||
// Env files
|
||||
'**/.env',
|
||||
'**/.env.*',
|
||||
'**/*.env',
|
||||
"**/.env",
|
||||
"**/.env.*",
|
||||
"**/*.env",
|
||||
// Misc
|
||||
'**/junit.xml',
|
||||
"**/junit.xml",
|
||||
];
|
||||
|
||||
async function readIgnoreFile(filePath) {
|
||||
try {
|
||||
if (!(await fs.pathExists(filePath))) return [];
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
if (!await fs.pathExists(filePath)) return [];
|
||||
const content = await fs.readFile(filePath, "utf8");
|
||||
return content
|
||||
.split('\n')
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l && !l.startsWith('#'));
|
||||
} catch {
|
||||
.filter((l) => l && !l.startsWith("#"));
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -153,18 +153,18 @@ async function parseGitignore(gitignorePath) {
|
||||
|
||||
async function loadIgnore(rootDir, extraPatterns = []) {
|
||||
const ig = ignore();
|
||||
const gitignorePath = path.join(rootDir, '.gitignore');
|
||||
const gitignorePath = path.join(rootDir, ".gitignore");
|
||||
const patterns = [
|
||||
...(await readIgnoreFile(gitignorePath)),
|
||||
...await readIgnoreFile(gitignorePath),
|
||||
...DEFAULT_PATTERNS,
|
||||
...extraPatterns,
|
||||
];
|
||||
// De-duplicate
|
||||
const unique = [...new Set(patterns.map(String))];
|
||||
const unique = Array.from(new Set(patterns.map((p) => String(p))));
|
||||
ig.add(unique);
|
||||
|
||||
// Include-only filter: return true if path should be included
|
||||
const filter = (relativePath) => !ig.ignores(relativePath.replaceAll('\\', '/'));
|
||||
const filter = (relativePath) => !ig.ignores(relativePath.replace(/\\/g, "/"));
|
||||
|
||||
return { ig, filter, patterns: unique };
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
const { Command } = require('commander');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const process = require('node:process');
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { Command } = require("commander");
|
||||
const fs = require("fs-extra");
|
||||
const path = require("node:path");
|
||||
const process = require("node:process");
|
||||
|
||||
// Modularized components
|
||||
const { findProjectRoot } = require('./projectRoot.js');
|
||||
const { promptYesNo, promptPath } = require('./prompts.js');
|
||||
const { discoverFiles, filterFiles, aggregateFileContents } = require('./files.js');
|
||||
const { generateXMLOutput } = require('./xml.js');
|
||||
const { calculateStatistics } = require('./stats.js');
|
||||
const { findProjectRoot } = require("./projectRoot.js");
|
||||
const { promptYesNo, promptPath } = require("./prompts.js");
|
||||
const {
|
||||
discoverFiles,
|
||||
filterFiles,
|
||||
aggregateFileContents,
|
||||
} = require("./files.js");
|
||||
const { generateXMLOutput } = require("./xml.js");
|
||||
const { calculateStatistics } = require("./stats.js");
|
||||
|
||||
/**
|
||||
* Recursively discover all files in a directory
|
||||
@@ -67,30 +73,30 @@ const { calculateStatistics } = require('./stats.js');
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('bmad-flatten')
|
||||
.description('BMad-Method codebase flattener tool')
|
||||
.version('1.0.0')
|
||||
.option('-i, --input <path>', 'Input directory to flatten', process.cwd())
|
||||
.option('-o, --output <path>', 'Output file path', 'flattened-codebase.xml')
|
||||
.name("bmad-flatten")
|
||||
.description("BMad-Method codebase flattener tool")
|
||||
.version("1.0.0")
|
||||
.option("-i, --input <path>", "Input directory to flatten", process.cwd())
|
||||
.option("-o, --output <path>", "Output file path", "flattened-codebase.xml")
|
||||
.action(async (options) => {
|
||||
let inputDir = path.resolve(options.input);
|
||||
let outputPath = path.resolve(options.output);
|
||||
|
||||
// Detect if user explicitly provided -i/--input or -o/--output
|
||||
const argv = process.argv.slice(2);
|
||||
const userSpecifiedInput = argv.some(
|
||||
(a) => a === '-i' || a === '--input' || a.startsWith('--input='),
|
||||
const userSpecifiedInput = argv.some((a) =>
|
||||
a === "-i" || a === "--input" || a.startsWith("--input=")
|
||||
);
|
||||
const userSpecifiedOutput = argv.some(
|
||||
(a) => a === '-o' || a === '--output' || a.startsWith('--output='),
|
||||
const userSpecifiedOutput = argv.some((a) =>
|
||||
a === "-o" || a === "--output" || a.startsWith("--output=")
|
||||
);
|
||||
const noPathArguments = !userSpecifiedInput && !userSpecifiedOutput;
|
||||
const noPathArgs = !userSpecifiedInput && !userSpecifiedOutput;
|
||||
|
||||
if (noPathArguments) {
|
||||
if (noPathArgs) {
|
||||
const detectedRoot = await findProjectRoot(process.cwd());
|
||||
const suggestedOutput = detectedRoot
|
||||
? path.join(detectedRoot, 'flattened-codebase.xml')
|
||||
: path.resolve('flattened-codebase.xml');
|
||||
? path.join(detectedRoot, "flattened-codebase.xml")
|
||||
: path.resolve("flattened-codebase.xml");
|
||||
|
||||
if (detectedRoot) {
|
||||
const useDefaults = await promptYesNo(
|
||||
@@ -101,23 +107,29 @@ program
|
||||
inputDir = detectedRoot;
|
||||
outputPath = suggestedOutput;
|
||||
} else {
|
||||
inputDir = await promptPath('Enter input directory path', process.cwd());
|
||||
inputDir = await promptPath(
|
||||
"Enter input directory path",
|
||||
process.cwd(),
|
||||
);
|
||||
outputPath = await promptPath(
|
||||
'Enter output file path',
|
||||
path.join(inputDir, 'flattened-codebase.xml'),
|
||||
"Enter output file path",
|
||||
path.join(inputDir, "flattened-codebase.xml"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log('Could not auto-detect a project root.');
|
||||
inputDir = await promptPath('Enter input directory path', process.cwd());
|
||||
console.log("Could not auto-detect a project root.");
|
||||
inputDir = await promptPath(
|
||||
"Enter input directory path",
|
||||
process.cwd(),
|
||||
);
|
||||
outputPath = await promptPath(
|
||||
'Enter output file path',
|
||||
path.join(inputDir, 'flattened-codebase.xml'),
|
||||
"Enter output file path",
|
||||
path.join(inputDir, "flattened-codebase.xml"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
@@ -125,25 +137,30 @@ program
|
||||
// Ensure output directory exists
|
||||
await fs.ensureDir(path.dirname(outputPath));
|
||||
|
||||
console.log(`Flattening codebase from: ${inputDir}`);
|
||||
console.log(`Output file: ${outputPath}`);
|
||||
|
||||
try {
|
||||
// Verify input directory exists
|
||||
if (!(await fs.pathExists(inputDir))) {
|
||||
if (!await fs.pathExists(inputDir)) {
|
||||
console.error(`❌ Error: Input directory does not exist: ${inputDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Import ora dynamically
|
||||
const { default: ora } = await import('ora');
|
||||
const { default: ora } = await import("ora");
|
||||
|
||||
// Start file discovery with spinner
|
||||
const discoverySpinner = ora('🔍 Discovering files...').start();
|
||||
const discoverySpinner = ora("🔍 Discovering files...").start();
|
||||
const files = await discoverFiles(inputDir);
|
||||
const filteredFiles = await filterFiles(files, inputDir);
|
||||
discoverySpinner.succeed(`📁 Found ${filteredFiles.length} files to include`);
|
||||
discoverySpinner.succeed(
|
||||
`📁 Found ${filteredFiles.length} files to include`,
|
||||
);
|
||||
|
||||
// Process files with progress tracking
|
||||
console.log('Reading file contents');
|
||||
const processingSpinner = ora('📄 Processing files...').start();
|
||||
console.log("Reading file contents");
|
||||
const processingSpinner = ora("📄 Processing files...").start();
|
||||
const aggregatedContent = await aggregateFileContents(
|
||||
filteredFiles,
|
||||
inputDir,
|
||||
@@ -155,413 +172,40 @@ program
|
||||
if (aggregatedContent.errors.length > 0) {
|
||||
console.log(`Errors: ${aggregatedContent.errors.length}`);
|
||||
}
|
||||
console.log(`Text files: ${aggregatedContent.textFiles.length}`);
|
||||
if (aggregatedContent.binaryFiles.length > 0) {
|
||||
console.log(`Binary files: ${aggregatedContent.binaryFiles.length}`);
|
||||
}
|
||||
|
||||
// Generate XML output using streaming
|
||||
const xmlSpinner = ora('🔧 Generating XML output...').start();
|
||||
const xmlSpinner = ora("🔧 Generating XML output...").start();
|
||||
await generateXMLOutput(aggregatedContent, outputPath);
|
||||
xmlSpinner.succeed('📝 XML generation completed');
|
||||
xmlSpinner.succeed("📝 XML generation completed");
|
||||
|
||||
// Calculate and display statistics
|
||||
const outputStats = await fs.stat(outputPath);
|
||||
const stats = await calculateStatistics(aggregatedContent, outputStats.size, inputDir);
|
||||
const stats = calculateStatistics(aggregatedContent, outputStats.size);
|
||||
|
||||
// Display completion summary
|
||||
console.log('\n📊 Completion Summary:');
|
||||
console.log("\n📊 Completion Summary:");
|
||||
console.log(
|
||||
`✅ Successfully processed ${filteredFiles.length} files into ${path.basename(outputPath)}`,
|
||||
`✅ Successfully processed ${filteredFiles.length} files into ${
|
||||
path.basename(outputPath)
|
||||
}`,
|
||||
);
|
||||
console.log(`📁 Output file: ${outputPath}`);
|
||||
console.log(`📏 Total source size: ${stats.totalSize}`);
|
||||
console.log(`📄 Generated XML size: ${stats.xmlSize}`);
|
||||
console.log(`📝 Total lines of code: ${stats.totalLines.toLocaleString()}`);
|
||||
console.log(
|
||||
`📝 Total lines of code: ${stats.totalLines.toLocaleString()}`,
|
||||
);
|
||||
console.log(`🔢 Estimated tokens: ${stats.estimatedTokens}`);
|
||||
console.log(
|
||||
`📊 File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors\n`,
|
||||
`📊 File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`,
|
||||
);
|
||||
|
||||
// Ask user if they want detailed stats + markdown report
|
||||
const generateDetailed = await promptYesNo(
|
||||
'Generate detailed stats (console + markdown) now?',
|
||||
true,
|
||||
);
|
||||
|
||||
if (generateDetailed) {
|
||||
// Additional detailed stats
|
||||
console.log('\n📈 Size Percentiles:');
|
||||
console.log(
|
||||
` Avg: ${Math.round(stats.avgFileSize).toLocaleString()} B, Median: ${Math.round(
|
||||
stats.medianFileSize,
|
||||
).toLocaleString()} B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
|
||||
);
|
||||
|
||||
if (Array.isArray(stats.histogram) && stats.histogram.length > 0) {
|
||||
console.log('\n🧮 Size Histogram:');
|
||||
for (const b of stats.histogram.slice(0, 2)) {
|
||||
console.log(` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`);
|
||||
}
|
||||
if (stats.histogram.length > 2) {
|
||||
console.log(` … and ${stats.histogram.length - 2} more buckets`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(stats.byExtension) && stats.byExtension.length > 0) {
|
||||
const topExt = stats.byExtension.slice(0, 2);
|
||||
console.log('\n📦 Top Extensions:');
|
||||
for (const e of topExt) {
|
||||
const pct = stats.totalBytes ? (e.bytes / stats.totalBytes) * 100 : 0;
|
||||
console.log(
|
||||
` ${e.ext}: ${e.count} files, ${e.bytes.toLocaleString()} bytes (${pct.toFixed(
|
||||
2,
|
||||
)}%)`,
|
||||
);
|
||||
}
|
||||
if (stats.byExtension.length > 2) {
|
||||
console.log(` … and ${stats.byExtension.length - 2} more extensions`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(stats.byDirectory) && stats.byDirectory.length > 0) {
|
||||
const topDir = stats.byDirectory.slice(0, 2);
|
||||
console.log('\n📂 Top Directories:');
|
||||
for (const d of topDir) {
|
||||
const pct = stats.totalBytes ? (d.bytes / stats.totalBytes) * 100 : 0;
|
||||
console.log(
|
||||
` ${d.dir}: ${d.count} files, ${d.bytes.toLocaleString()} bytes (${pct.toFixed(
|
||||
2,
|
||||
)}%)`,
|
||||
);
|
||||
}
|
||||
if (stats.byDirectory.length > 2) {
|
||||
console.log(` … and ${stats.byDirectory.length - 2} more directories`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(stats.depthDistribution) && stats.depthDistribution.length > 0) {
|
||||
console.log('\n🌳 Depth Distribution:');
|
||||
const dd = stats.depthDistribution.slice(0, 2);
|
||||
let line = ' ' + dd.map((d) => `${d.depth}:${d.count}`).join(' ');
|
||||
if (stats.depthDistribution.length > 2) {
|
||||
line += ` … +${stats.depthDistribution.length - 2} more`;
|
||||
}
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
if (Array.isArray(stats.longestPaths) && stats.longestPaths.length > 0) {
|
||||
console.log('\n🧵 Longest Paths:');
|
||||
for (const p of stats.longestPaths.slice(0, 2)) {
|
||||
console.log(` ${p.path} (${p.length} chars, ${p.size.toLocaleString()} bytes)`);
|
||||
}
|
||||
if (stats.longestPaths.length > 2) {
|
||||
console.log(` … and ${stats.longestPaths.length - 2} more paths`);
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.temporal) {
|
||||
console.log('\n⏱️ Temporal:');
|
||||
if (stats.temporal.oldest) {
|
||||
console.log(
|
||||
` Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`,
|
||||
);
|
||||
}
|
||||
if (stats.temporal.newest) {
|
||||
console.log(
|
||||
` Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`,
|
||||
);
|
||||
}
|
||||
if (Array.isArray(stats.temporal.ageBuckets)) {
|
||||
console.log(' Age buckets:');
|
||||
for (const b of stats.temporal.ageBuckets.slice(0, 2)) {
|
||||
console.log(` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`);
|
||||
}
|
||||
if (stats.temporal.ageBuckets.length > 2) {
|
||||
console.log(` … and ${stats.temporal.ageBuckets.length - 2} more buckets`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.quality) {
|
||||
console.log('\n✅ Quality Signals:');
|
||||
console.log(` Zero-byte files: ${stats.quality.zeroByteFiles}`);
|
||||
console.log(` Empty text files: ${stats.quality.emptyTextFiles}`);
|
||||
console.log(` Hidden files: ${stats.quality.hiddenFiles}`);
|
||||
console.log(` Symlinks: ${stats.quality.symlinks}`);
|
||||
console.log(
|
||||
` Large files (>= ${(stats.quality.largeThreshold / (1024 * 1024)).toFixed(
|
||||
0,
|
||||
)} MB): ${stats.quality.largeFilesCount}`,
|
||||
);
|
||||
console.log(
|
||||
` Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(stats.duplicateCandidates) && stats.duplicateCandidates.length > 0) {
|
||||
console.log('\n🧬 Duplicate Candidates:');
|
||||
for (const d of stats.duplicateCandidates.slice(0, 2)) {
|
||||
console.log(` ${d.reason}: ${d.count} files @ ${d.size.toLocaleString()} bytes`);
|
||||
}
|
||||
if (stats.duplicateCandidates.length > 2) {
|
||||
console.log(` … and ${stats.duplicateCandidates.length - 2} more groups`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof stats.compressibilityRatio === 'number') {
|
||||
console.log(
|
||||
`\n🗜️ Compressibility ratio (sampled): ${(stats.compressibilityRatio * 100).toFixed(
|
||||
2,
|
||||
)}%`,
|
||||
);
|
||||
}
|
||||
|
||||
if (stats.git && stats.git.isRepo) {
|
||||
console.log('\n🔧 Git:');
|
||||
console.log(
|
||||
` Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`,
|
||||
);
|
||||
console.log(
|
||||
` Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`,
|
||||
);
|
||||
if (Array.isArray(stats.git.lfsCandidates) && stats.git.lfsCandidates.length > 0) {
|
||||
console.log(' LFS candidates (top 2):');
|
||||
for (const f of stats.git.lfsCandidates.slice(0, 2)) {
|
||||
console.log(` ${f.path} (${f.size.toLocaleString()} bytes)`);
|
||||
}
|
||||
if (stats.git.lfsCandidates.length > 2) {
|
||||
console.log(` … and ${stats.git.lfsCandidates.length - 2} more`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(stats.largestFiles) && stats.largestFiles.length > 0) {
|
||||
console.log('\n📚 Largest Files (top 2):');
|
||||
for (const f of stats.largestFiles.slice(0, 2)) {
|
||||
// Show LOC for text files when available; omit ext and mtime
|
||||
let locStr = '';
|
||||
if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) {
|
||||
const tf = aggregatedContent.textFiles.find((t) => t.path === f.path);
|
||||
if (tf && typeof tf.lines === 'number') {
|
||||
locStr = `, LOC: ${tf.lines.toLocaleString()}`;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
` ${f.path} – ${f.sizeFormatted} (${f.percentOfTotal.toFixed(2)}%)${locStr}`,
|
||||
);
|
||||
}
|
||||
if (stats.largestFiles.length > 2) {
|
||||
console.log(` … and ${stats.largestFiles.length - 2} more files`);
|
||||
}
|
||||
}
|
||||
|
||||
// Write a comprehensive markdown report next to the XML
|
||||
{
|
||||
const mdPath = outputPath.endsWith('.xml')
|
||||
? outputPath.replace(/\.xml$/i, '.stats.md')
|
||||
: outputPath + '.stats.md';
|
||||
try {
|
||||
const pct = (num, den) => (den ? (num / den) * 100 : 0);
|
||||
const md = [];
|
||||
md.push(
|
||||
`# 🧾 Flatten Stats for ${path.basename(outputPath)}`,
|
||||
'',
|
||||
'## 📊 Summary',
|
||||
`- Total source size: ${stats.totalSize}`,
|
||||
`- Generated XML size: ${stats.xmlSize}`,
|
||||
`- Total lines of code: ${stats.totalLines.toLocaleString()}`,
|
||||
`- Estimated tokens: ${stats.estimatedTokens}`,
|
||||
`- File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`,
|
||||
'',
|
||||
'## 📈 Size Percentiles',
|
||||
`Avg: ${Math.round(stats.avgFileSize).toLocaleString()} B, Median: ${Math.round(
|
||||
stats.medianFileSize,
|
||||
).toLocaleString()} B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
|
||||
'',
|
||||
);
|
||||
|
||||
// Histogram
|
||||
if (Array.isArray(stats.histogram) && stats.histogram.length > 0) {
|
||||
md.push(
|
||||
'## 🧮 Size Histogram',
|
||||
'| Bucket | Files | Bytes |',
|
||||
'| --- | ---: | ---: |',
|
||||
);
|
||||
for (const b of stats.histogram) {
|
||||
md.push(`| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`);
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
|
||||
// Top Extensions
|
||||
if (Array.isArray(stats.byExtension) && stats.byExtension.length > 0) {
|
||||
md.push(
|
||||
'## 📦 Top Extensions by Bytes (Top 20)',
|
||||
'| Ext | Files | Bytes | % of total |',
|
||||
'| --- | ---: | ---: | ---: |',
|
||||
);
|
||||
for (const e of stats.byExtension.slice(0, 20)) {
|
||||
const p = pct(e.bytes, stats.totalBytes);
|
||||
md.push(
|
||||
`| ${e.ext} | ${e.count} | ${e.bytes.toLocaleString()} | ${p.toFixed(2)}% |`,
|
||||
);
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
|
||||
// Top Directories
|
||||
if (Array.isArray(stats.byDirectory) && stats.byDirectory.length > 0) {
|
||||
md.push(
|
||||
'## 📂 Top Directories by Bytes (Top 20)',
|
||||
'| Directory | Files | Bytes | % of total |',
|
||||
'| --- | ---: | ---: | ---: |',
|
||||
);
|
||||
for (const d of stats.byDirectory.slice(0, 20)) {
|
||||
const p = pct(d.bytes, stats.totalBytes);
|
||||
md.push(
|
||||
`| ${d.dir} | ${d.count} | ${d.bytes.toLocaleString()} | ${p.toFixed(2)}% |`,
|
||||
);
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
|
||||
// Depth distribution
|
||||
if (Array.isArray(stats.depthDistribution) && stats.depthDistribution.length > 0) {
|
||||
md.push('## 🌳 Depth Distribution', '| Depth | Count |', '| ---: | ---: |');
|
||||
for (const d of stats.depthDistribution) {
|
||||
md.push(`| ${d.depth} | ${d.count} |`);
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
|
||||
// Longest paths
|
||||
if (Array.isArray(stats.longestPaths) && stats.longestPaths.length > 0) {
|
||||
md.push(
|
||||
'## 🧵 Longest Paths (Top 25)',
|
||||
'| Path | Length | Bytes |',
|
||||
'| --- | ---: | ---: |',
|
||||
);
|
||||
for (const pth of stats.longestPaths) {
|
||||
md.push(`| ${pth.path} | ${pth.length} | ${pth.size.toLocaleString()} |`);
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
|
||||
// Temporal
|
||||
if (stats.temporal) {
|
||||
md.push('## ⏱️ Temporal');
|
||||
if (stats.temporal.oldest) {
|
||||
md.push(`- Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`);
|
||||
}
|
||||
if (stats.temporal.newest) {
|
||||
md.push(`- Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`);
|
||||
}
|
||||
if (Array.isArray(stats.temporal.ageBuckets)) {
|
||||
md.push('', '| Age | Files | Bytes |', '| --- | ---: | ---: |');
|
||||
for (const b of stats.temporal.ageBuckets) {
|
||||
md.push(`| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`);
|
||||
}
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
|
||||
// Quality signals
|
||||
if (stats.quality) {
|
||||
md.push(
|
||||
'## ✅ Quality Signals',
|
||||
`- Zero-byte files: ${stats.quality.zeroByteFiles}`,
|
||||
`- Empty text files: ${stats.quality.emptyTextFiles}`,
|
||||
`- Hidden files: ${stats.quality.hiddenFiles}`,
|
||||
`- Symlinks: ${stats.quality.symlinks}`,
|
||||
`- Large files (>= ${(stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)} MB): ${stats.quality.largeFilesCount}`,
|
||||
`- Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`,
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
// Duplicates
|
||||
if (Array.isArray(stats.duplicateCandidates) && stats.duplicateCandidates.length > 0) {
|
||||
md.push(
|
||||
'## 🧬 Duplicate Candidates',
|
||||
'| Reason | Files | Size (bytes) |',
|
||||
'| --- | ---: | ---: |',
|
||||
);
|
||||
for (const d of stats.duplicateCandidates) {
|
||||
md.push(`| ${d.reason} | ${d.count} | ${d.size.toLocaleString()} |`);
|
||||
}
|
||||
md.push('', '### 🧬 Duplicate Groups Details');
|
||||
let dupIndex = 1;
|
||||
for (const d of stats.duplicateCandidates) {
|
||||
md.push(
|
||||
`#### Group ${dupIndex}: ${d.count} files @ ${d.size.toLocaleString()} bytes (${d.reason})`,
|
||||
);
|
||||
if (Array.isArray(d.files) && d.files.length > 0) {
|
||||
for (const fp of d.files) {
|
||||
md.push(`- ${fp}`);
|
||||
}
|
||||
} else {
|
||||
md.push('- (file list unavailable)');
|
||||
}
|
||||
md.push('');
|
||||
dupIndex++;
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
|
||||
// Compressibility
|
||||
if (typeof stats.compressibilityRatio === 'number') {
|
||||
md.push(
|
||||
'## 🗜️ Compressibility',
|
||||
`Sampled compressibility ratio: ${(stats.compressibilityRatio * 100).toFixed(2)}%`,
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
// Git
|
||||
if (stats.git && stats.git.isRepo) {
|
||||
md.push(
|
||||
'## 🔧 Git',
|
||||
`- Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`,
|
||||
`- Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`,
|
||||
);
|
||||
if (Array.isArray(stats.git.lfsCandidates) && stats.git.lfsCandidates.length > 0) {
|
||||
md.push('', '### 📦 LFS Candidates (Top 20)', '| Path | Bytes |', '| --- | ---: |');
|
||||
for (const f of stats.git.lfsCandidates.slice(0, 20)) {
|
||||
md.push(`| ${f.path} | ${f.size.toLocaleString()} |`);
|
||||
}
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
|
||||
// Largest Files
|
||||
if (Array.isArray(stats.largestFiles) && stats.largestFiles.length > 0) {
|
||||
md.push(
|
||||
'## 📚 Largest Files (Top 50)',
|
||||
'| Path | Size | % of total | LOC |',
|
||||
'| --- | ---: | ---: | ---: |',
|
||||
);
|
||||
for (const f of stats.largestFiles) {
|
||||
let loc = '';
|
||||
if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) {
|
||||
const tf = aggregatedContent.textFiles.find((t) => t.path === f.path);
|
||||
if (tf && typeof tf.lines === 'number') {
|
||||
loc = tf.lines.toLocaleString();
|
||||
}
|
||||
}
|
||||
md.push(
|
||||
`| ${f.path} | ${f.sizeFormatted} | ${f.percentOfTotal.toFixed(2)}% | ${loc} |`,
|
||||
);
|
||||
}
|
||||
md.push('');
|
||||
}
|
||||
|
||||
await fs.writeFile(mdPath, md.join('\n'));
|
||||
console.log(`\n🧾 Detailed stats report written to: ${mdPath}`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Failed to write stats markdown: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Critical error:', error.message);
|
||||
console.error('An unexpected error occurred.');
|
||||
console.error("❌ Critical error:", error.message);
|
||||
console.error("An unexpected error occurred.");
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,203 +1,42 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
|
||||
// Deno/Node compatibility: explicitly import process
|
||||
const process = require('node:process');
|
||||
const { execFile } = require('node:child_process');
|
||||
const { promisify } = require('node:util');
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
// Simple memoization across calls (keyed by realpath of startDir)
|
||||
const _cache = new Map();
|
||||
|
||||
async function _tryRun(cmd, args, cwd, timeoutMs = 500) {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(cmd, args, {
|
||||
cwd,
|
||||
timeout: timeoutMs,
|
||||
windowsHide: true,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
const out = String(stdout || '').trim();
|
||||
return out || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function _detectVcsTopLevel(startDir) {
|
||||
// Run common VCS root queries in parallel; ignore failures
|
||||
const gitP = _tryRun('git', ['rev-parse', '--show-toplevel'], startDir);
|
||||
const hgP = _tryRun('hg', ['root'], startDir);
|
||||
const svnP = (async () => {
|
||||
const show = await _tryRun('svn', ['info', '--show-item', 'wc-root'], startDir);
|
||||
if (show) return show;
|
||||
const info = await _tryRun('svn', ['info'], startDir);
|
||||
if (info) {
|
||||
const line = info
|
||||
.split(/\r?\n/)
|
||||
.find((l) => l.toLowerCase().startsWith('working copy root path:'));
|
||||
if (line) return line.split(':').slice(1).join(':').trim();
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
const [git, hg, svn] = await Promise.all([gitP, hgP, svnP]);
|
||||
return git || hg || svn || null;
|
||||
}
|
||||
const fs = require("fs-extra");
|
||||
const path = require("node:path");
|
||||
|
||||
/**
|
||||
* Attempt to find the project root by walking up from startDir.
|
||||
* Uses a robust, prioritized set of ecosystem markers (VCS > workspaces/monorepo > lock/build > language config).
|
||||
* Also recognizes package.json with "workspaces" as a workspace root.
|
||||
* You can augment markers via env PROJECT_ROOT_MARKERS as a comma-separated list of file/dir names.
|
||||
* Attempt to find the project root by walking up from startDir
|
||||
* Looks for common project markers like .git, package.json, pyproject.toml, etc.
|
||||
* @param {string} startDir
|
||||
* @returns {Promise<string|null>} project root directory or null if not found
|
||||
*/
|
||||
async function findProjectRoot(startDir) {
|
||||
try {
|
||||
// Resolve symlinks for robustness (e.g., when invoked from a symlinked path)
|
||||
let dir = path.resolve(startDir);
|
||||
try {
|
||||
dir = await fs.realpath(dir);
|
||||
} catch {
|
||||
// ignore if realpath fails; continue with resolved path
|
||||
}
|
||||
const startKey = dir; // preserve starting point for caching
|
||||
if (_cache.has(startKey)) return _cache.get(startKey);
|
||||
const fsRoot = path.parse(dir).root;
|
||||
|
||||
// Helper to safely check for existence
|
||||
const exists = (p) => fs.pathExists(p);
|
||||
|
||||
// Build checks: an array of { makePath: (dir) => string, weight }
|
||||
const checks = [];
|
||||
|
||||
const add = (rel, weight) => {
|
||||
const makePath = (d) => (Array.isArray(rel) ? path.join(d, ...rel) : path.join(d, rel));
|
||||
checks.push({ makePath, weight });
|
||||
};
|
||||
|
||||
// Highest priority: explicit sentinel markers
|
||||
add('.project-root', 110);
|
||||
add('.workspace-root', 110);
|
||||
add('.repo-root', 110);
|
||||
|
||||
// Highest priority: VCS roots
|
||||
add('.git', 100);
|
||||
add('.hg', 95);
|
||||
add('.svn', 95);
|
||||
|
||||
// Monorepo/workspace indicators
|
||||
add('pnpm-workspace.yaml', 90);
|
||||
add('lerna.json', 90);
|
||||
add('turbo.json', 90);
|
||||
add('nx.json', 90);
|
||||
add('rush.json', 90);
|
||||
add('go.work', 90);
|
||||
add('WORKSPACE', 90);
|
||||
add('WORKSPACE.bazel', 90);
|
||||
add('MODULE.bazel', 90);
|
||||
add('pants.toml', 90);
|
||||
|
||||
// Lockfiles and package-manager/top-level locks
|
||||
add('yarn.lock', 85);
|
||||
add('pnpm-lock.yaml', 85);
|
||||
add('package-lock.json', 85);
|
||||
add('bun.lockb', 85);
|
||||
add('Cargo.lock', 85);
|
||||
add('composer.lock', 85);
|
||||
add('poetry.lock', 85);
|
||||
add('Pipfile.lock', 85);
|
||||
add('Gemfile.lock', 85);
|
||||
|
||||
// Build-system root indicators
|
||||
add('settings.gradle', 80);
|
||||
add('settings.gradle.kts', 80);
|
||||
add('gradlew', 80);
|
||||
add('pom.xml', 80);
|
||||
add('build.sbt', 80);
|
||||
add(['project', 'build.properties'], 80);
|
||||
|
||||
// Language/project config markers
|
||||
add('deno.json', 75);
|
||||
add('deno.jsonc', 75);
|
||||
add('pyproject.toml', 75);
|
||||
add('Pipfile', 75);
|
||||
add('requirements.txt', 75);
|
||||
add('go.mod', 75);
|
||||
add('Cargo.toml', 75);
|
||||
add('composer.json', 75);
|
||||
add('mix.exs', 75);
|
||||
add('Gemfile', 75);
|
||||
add('CMakeLists.txt', 75);
|
||||
add('stack.yaml', 75);
|
||||
add('cabal.project', 75);
|
||||
add('rebar.config', 75);
|
||||
add('pubspec.yaml', 75);
|
||||
add('flake.nix', 75);
|
||||
add('shell.nix', 75);
|
||||
add('default.nix', 75);
|
||||
add('.tool-versions', 75);
|
||||
add('package.json', 74); // generic Node project (lower than lockfiles/workspaces)
|
||||
|
||||
// Changesets
|
||||
add(['.changeset', 'config.json'], 70);
|
||||
add('.changeset', 70);
|
||||
|
||||
// Custom markers via env (comma-separated names)
|
||||
if (process.env.PROJECT_ROOT_MARKERS) {
|
||||
for (const name of process.env.PROJECT_ROOT_MARKERS.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)) {
|
||||
add(name, 72);
|
||||
}
|
||||
}
|
||||
|
||||
/** Check for package.json with "workspaces" */
|
||||
const hasWorkspacePackageJson = async (d) => {
|
||||
const pkgPath = path.join(d, 'package.json');
|
||||
if (!(await exists(pkgPath))) return false;
|
||||
try {
|
||||
const raw = await fs.readFile(pkgPath, 'utf8');
|
||||
const pkg = JSON.parse(raw);
|
||||
return Boolean(pkg && pkg.workspaces);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let best = null; // { dir, weight }
|
||||
|
||||
// Try to detect VCS toplevel once up-front; treat as authoritative slightly above .git marker
|
||||
const vcsTop = await _detectVcsTopLevel(dir);
|
||||
if (vcsTop) {
|
||||
best = { dir: vcsTop, weight: 101 };
|
||||
}
|
||||
const root = path.parse(dir).root;
|
||||
const markers = [
|
||||
".git",
|
||||
"package.json",
|
||||
"pnpm-workspace.yaml",
|
||||
"yarn.lock",
|
||||
"pnpm-lock.yaml",
|
||||
"pyproject.toml",
|
||||
"requirements.txt",
|
||||
"go.mod",
|
||||
"Cargo.toml",
|
||||
"composer.json",
|
||||
".hg",
|
||||
".svn",
|
||||
];
|
||||
|
||||
while (true) {
|
||||
// Special check: package.json with "workspaces"
|
||||
if ((await hasWorkspacePackageJson(dir)) && (!best || 90 >= best.weight))
|
||||
best = { dir, weight: 90 };
|
||||
|
||||
// Evaluate all other checks in parallel
|
||||
const results = await Promise.all(
|
||||
checks.map(async (c) => ({ c, ok: await exists(c.makePath(dir)) })),
|
||||
const exists = await Promise.all(
|
||||
markers.map((m) => fs.pathExists(path.join(dir, m))),
|
||||
);
|
||||
|
||||
for (const { c, ok } of results) {
|
||||
if (!ok) continue;
|
||||
if (!best || c.weight >= best.weight) {
|
||||
best = { dir, weight: c.weight };
|
||||
}
|
||||
if (exists.some(Boolean)) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
if (dir === fsRoot) break;
|
||||
if (dir === root) break;
|
||||
dir = path.dirname(dir);
|
||||
}
|
||||
|
||||
const out = best ? best.dir : null;
|
||||
_cache.set(startKey, out);
|
||||
return out;
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const readline = require('node:readline');
|
||||
const process = require('node:process');
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const readline = require("node:readline");
|
||||
const process = require("node:process");
|
||||
|
||||
function expandHome(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;
|
||||
}
|
||||
|
||||
@@ -27,16 +27,16 @@ function promptQuestion(question) {
|
||||
}
|
||||
|
||||
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();
|
||||
if (!ans) return defaultYes;
|
||||
if (['y', 'yes'].includes(ans)) return true;
|
||||
if (['n', 'no'].includes(ans)) return false;
|
||||
if (["y", "yes"].includes(ans)) return true;
|
||||
if (["n", "no"].includes(ans)) return false;
|
||||
return promptYesNo(question, defaultYes);
|
||||
}
|
||||
|
||||
async function promptPath(question, defaultValue) {
|
||||
const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` : ''}: `;
|
||||
const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` : ""}: `;
|
||||
const ans = (await promptQuestion(prompt)).trim();
|
||||
return expandHome(ans || defaultValue);
|
||||
}
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const zlib = require('node:zlib');
|
||||
const { Buffer } = require('node:buffer');
|
||||
const crypto = require('node:crypto');
|
||||
const cp = require('node:child_process');
|
||||
|
||||
const KB = 1024;
|
||||
const MB = 1024 * KB;
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
};
|
||||
|
||||
const percentile = (sorted, p) => {
|
||||
if (sorted.length === 0) return 0;
|
||||
const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
|
||||
return sorted[idx];
|
||||
};
|
||||
|
||||
async function processWithLimit(items, fn, concurrency = 64) {
|
||||
for (let i = 0; i < items.length; i += concurrency) {
|
||||
await Promise.all(items.slice(i, i + concurrency).map(fn));
|
||||
}
|
||||
}
|
||||
|
||||
async function enrichAllFiles(textFiles, binaryFiles) {
|
||||
/** @type {Array<{ path: string; absolutePath: string; size: number; lines?: number; isBinary: boolean; ext: string; dir: string; depth: number; hidden: boolean; mtimeMs: number; isSymlink: boolean; }>} */
|
||||
const allFiles = [];
|
||||
|
||||
async function enrich(file, isBinary) {
|
||||
const ext = (path.extname(file.path) || '').toLowerCase();
|
||||
const dir = path.dirname(file.path) || '.';
|
||||
const depth = file.path.split(path.sep).filter(Boolean).length;
|
||||
const hidden = file.path.split(path.sep).some((seg) => seg.startsWith('.'));
|
||||
let mtimeMs = 0;
|
||||
let isSymlink = false;
|
||||
try {
|
||||
const lst = await fs.lstat(file.absolutePath);
|
||||
mtimeMs = lst.mtimeMs;
|
||||
isSymlink = lst.isSymbolicLink();
|
||||
} catch {
|
||||
/* ignore lstat errors during enrichment */
|
||||
}
|
||||
allFiles.push({
|
||||
path: file.path,
|
||||
absolutePath: file.absolutePath,
|
||||
size: file.size || 0,
|
||||
lines: file.lines,
|
||||
isBinary,
|
||||
ext,
|
||||
dir,
|
||||
depth,
|
||||
hidden,
|
||||
mtimeMs,
|
||||
isSymlink,
|
||||
});
|
||||
}
|
||||
|
||||
await processWithLimit(textFiles, (f) => enrich(f, false));
|
||||
await processWithLimit(binaryFiles, (f) => enrich(f, true));
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
function buildHistogram(allFiles) {
|
||||
const buckets = [
|
||||
[1 * KB, '0–1KB'],
|
||||
[10 * KB, '1–10KB'],
|
||||
[100 * KB, '10–100KB'],
|
||||
[1 * MB, '100KB–1MB'],
|
||||
[10 * MB, '1–10MB'],
|
||||
[100 * MB, '10–100MB'],
|
||||
[Infinity, '>=100MB'],
|
||||
];
|
||||
const histogram = buckets.map(([_, label]) => ({ label, count: 0, bytes: 0 }));
|
||||
for (const f of allFiles) {
|
||||
for (const [i, bucket] of buckets.entries()) {
|
||||
if (f.size < bucket[0]) {
|
||||
histogram[i].count++;
|
||||
histogram[i].bytes += f.size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return histogram;
|
||||
}
|
||||
|
||||
function aggregateByExtension(allFiles) {
|
||||
const byExtension = new Map();
|
||||
for (const f of allFiles) {
|
||||
const key = f.ext || '<none>';
|
||||
const v = byExtension.get(key) || { ext: key, count: 0, bytes: 0 };
|
||||
v.count++;
|
||||
v.bytes += f.size;
|
||||
byExtension.set(key, v);
|
||||
}
|
||||
return [...byExtension.values()].sort((a, b) => b.bytes - a.bytes);
|
||||
}
|
||||
|
||||
function aggregateByDirectory(allFiles) {
|
||||
const byDirectory = new Map();
|
||||
function addDirBytes(dir, bytes) {
|
||||
const v = byDirectory.get(dir) || { dir, count: 0, bytes: 0 };
|
||||
v.count++;
|
||||
v.bytes += bytes;
|
||||
byDirectory.set(dir, v);
|
||||
}
|
||||
for (const f of allFiles) {
|
||||
const parts = f.dir === '.' ? [] : f.dir.split(path.sep);
|
||||
let acc = '';
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
acc = i === 0 ? parts[0] : acc + path.sep + parts[i];
|
||||
addDirBytes(acc, f.size);
|
||||
}
|
||||
if (parts.length === 0) addDirBytes('.', f.size);
|
||||
}
|
||||
return [...byDirectory.values()].sort((a, b) => b.bytes - a.bytes);
|
||||
}
|
||||
|
||||
function computeDepthAndLongest(allFiles) {
|
||||
const depthDistribution = new Map();
|
||||
for (const f of allFiles) {
|
||||
depthDistribution.set(f.depth, (depthDistribution.get(f.depth) || 0) + 1);
|
||||
}
|
||||
const longestPaths = [...allFiles]
|
||||
.sort((a, b) => b.path.length - a.path.length)
|
||||
.slice(0, 25)
|
||||
.map((f) => ({ path: f.path, length: f.path.length, size: f.size }));
|
||||
const depthDist = [...depthDistribution.entries()]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([depth, count]) => ({ depth, count }));
|
||||
return { depthDist, longestPaths };
|
||||
}
|
||||
|
||||
function computeTemporal(allFiles, nowMs) {
|
||||
let oldest = null,
|
||||
newest = null;
|
||||
const ageBuckets = [
|
||||
{ label: '> 1 year', minDays: 365, maxDays: Infinity, count: 0, bytes: 0 },
|
||||
{ label: '6–12 months', minDays: 180, maxDays: 365, count: 0, bytes: 0 },
|
||||
{ label: '1–6 months', minDays: 30, maxDays: 180, count: 0, bytes: 0 },
|
||||
{ label: '7–30 days', minDays: 7, maxDays: 30, count: 0, bytes: 0 },
|
||||
{ label: '1–7 days', minDays: 1, maxDays: 7, count: 0, bytes: 0 },
|
||||
{ label: '< 1 day', minDays: 0, maxDays: 1, count: 0, bytes: 0 },
|
||||
];
|
||||
for (const f of allFiles) {
|
||||
const ageDays = Math.max(0, (nowMs - (f.mtimeMs || nowMs)) / (24 * 60 * 60 * 1000));
|
||||
for (const b of ageBuckets) {
|
||||
if (ageDays >= b.minDays && ageDays < b.maxDays) {
|
||||
b.count++;
|
||||
b.bytes += f.size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!oldest || f.mtimeMs < oldest.mtimeMs) oldest = f;
|
||||
if (!newest || f.mtimeMs > newest.mtimeMs) newest = f;
|
||||
}
|
||||
return {
|
||||
oldest: oldest
|
||||
? { path: oldest.path, mtime: oldest.mtimeMs ? new Date(oldest.mtimeMs).toISOString() : null }
|
||||
: null,
|
||||
newest: newest
|
||||
? { path: newest.path, mtime: newest.mtimeMs ? new Date(newest.mtimeMs).toISOString() : null }
|
||||
: null,
|
||||
ageBuckets,
|
||||
};
|
||||
}
|
||||
|
||||
function computeQuality(allFiles, textFiles) {
|
||||
const zeroByteFiles = allFiles.filter((f) => f.size === 0).length;
|
||||
const emptyTextFiles = textFiles.filter(
|
||||
(f) => (f.size || 0) === 0 || (f.lines || 0) === 0,
|
||||
).length;
|
||||
const hiddenFiles = allFiles.filter((f) => f.hidden).length;
|
||||
const symlinks = allFiles.filter((f) => f.isSymlink).length;
|
||||
const largeThreshold = 50 * MB;
|
||||
const suspiciousThreshold = 100 * MB;
|
||||
const largeFilesCount = allFiles.filter((f) => f.size >= largeThreshold).length;
|
||||
const suspiciousLargeFilesCount = allFiles.filter((f) => f.size >= suspiciousThreshold).length;
|
||||
return {
|
||||
zeroByteFiles,
|
||||
emptyTextFiles,
|
||||
hiddenFiles,
|
||||
symlinks,
|
||||
largeFilesCount,
|
||||
suspiciousLargeFilesCount,
|
||||
largeThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
function computeDuplicates(allFiles, textFiles) {
|
||||
const duplicatesBySize = new Map();
|
||||
for (const f of allFiles) {
|
||||
const key = String(f.size);
|
||||
const arr = duplicatesBySize.get(key) || [];
|
||||
arr.push(f);
|
||||
duplicatesBySize.set(key, arr);
|
||||
}
|
||||
const duplicateCandidates = [];
|
||||
for (const [sizeKey, arr] of duplicatesBySize.entries()) {
|
||||
if (arr.length < 2) continue;
|
||||
const textGroup = arr.filter((f) => !f.isBinary);
|
||||
const otherGroup = arr.filter((f) => f.isBinary);
|
||||
const contentHashGroups = new Map();
|
||||
for (const tf of textGroup) {
|
||||
try {
|
||||
const src = textFiles.find((x) => x.absolutePath === tf.absolutePath);
|
||||
const content = src ? src.content : '';
|
||||
const h = crypto.createHash('sha1').update(content).digest('hex');
|
||||
const g = contentHashGroups.get(h) || [];
|
||||
g.push(tf);
|
||||
contentHashGroups.set(h, g);
|
||||
} catch {
|
||||
/* ignore hashing errors for duplicate detection */
|
||||
}
|
||||
}
|
||||
for (const [_h, g] of contentHashGroups.entries()) {
|
||||
if (g.length > 1)
|
||||
duplicateCandidates.push({
|
||||
reason: 'same-size+text-hash',
|
||||
size: Number(sizeKey),
|
||||
count: g.length,
|
||||
files: g.map((f) => f.path),
|
||||
});
|
||||
}
|
||||
if (otherGroup.length > 1) {
|
||||
duplicateCandidates.push({
|
||||
reason: 'same-size',
|
||||
size: Number(sizeKey),
|
||||
count: otherGroup.length,
|
||||
files: otherGroup.map((f) => f.path),
|
||||
});
|
||||
}
|
||||
}
|
||||
return duplicateCandidates;
|
||||
}
|
||||
|
||||
function estimateCompressibility(textFiles) {
|
||||
let compSampleBytes = 0;
|
||||
let compCompressedBytes = 0;
|
||||
for (const tf of textFiles) {
|
||||
try {
|
||||
const sampleLen = Math.min(256 * 1024, tf.size || 0);
|
||||
if (sampleLen <= 0) continue;
|
||||
const sample = tf.content.slice(0, sampleLen);
|
||||
const gz = zlib.gzipSync(Buffer.from(sample, 'utf8'));
|
||||
compSampleBytes += sampleLen;
|
||||
compCompressedBytes += gz.length;
|
||||
} catch {
|
||||
/* ignore compression errors during sampling */
|
||||
}
|
||||
}
|
||||
return compSampleBytes > 0 ? compCompressedBytes / compSampleBytes : null;
|
||||
}
|
||||
|
||||
function computeGitInfo(allFiles, rootDir, largeThreshold) {
|
||||
const info = {
|
||||
isRepo: false,
|
||||
trackedCount: 0,
|
||||
trackedBytes: 0,
|
||||
untrackedCount: 0,
|
||||
untrackedBytes: 0,
|
||||
lfsCandidates: [],
|
||||
};
|
||||
try {
|
||||
if (!rootDir) return info;
|
||||
const top = cp
|
||||
.execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
||||
cwd: rootDir,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
})
|
||||
.toString()
|
||||
.trim();
|
||||
if (!top) return info;
|
||||
info.isRepo = true;
|
||||
const out = cp.execFileSync('git', ['ls-files', '-z'], {
|
||||
cwd: rootDir,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
const tracked = new Set(out.toString().split('\0').filter(Boolean));
|
||||
let trackedBytes = 0,
|
||||
trackedCount = 0,
|
||||
untrackedBytes = 0,
|
||||
untrackedCount = 0;
|
||||
const lfsCandidates = [];
|
||||
for (const f of allFiles) {
|
||||
const isTracked = tracked.has(f.path);
|
||||
if (isTracked) {
|
||||
trackedCount++;
|
||||
trackedBytes += f.size;
|
||||
if (f.size >= largeThreshold) lfsCandidates.push({ path: f.path, size: f.size });
|
||||
} else {
|
||||
untrackedCount++;
|
||||
untrackedBytes += f.size;
|
||||
}
|
||||
}
|
||||
info.trackedCount = trackedCount;
|
||||
info.trackedBytes = trackedBytes;
|
||||
info.untrackedCount = untrackedCount;
|
||||
info.untrackedBytes = untrackedBytes;
|
||||
info.lfsCandidates = lfsCandidates.sort((a, b) => b.size - a.size).slice(0, 50);
|
||||
} catch {
|
||||
/* git not available or not a repo, ignore */
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
function computeLargestFiles(allFiles, totalBytes) {
|
||||
const toPct = (num, den) => (den === 0 ? 0 : (num / den) * 100);
|
||||
return [...allFiles]
|
||||
.sort((a, b) => b.size - a.size)
|
||||
.slice(0, 50)
|
||||
.map((f) => ({
|
||||
path: f.path,
|
||||
size: f.size,
|
||||
sizeFormatted: formatSize(f.size),
|
||||
percentOfTotal: toPct(f.size, totalBytes),
|
||||
ext: f.ext || '',
|
||||
isBinary: f.isBinary,
|
||||
mtime: f.mtimeMs ? new Date(f.mtimeMs).toISOString() : null,
|
||||
}));
|
||||
}
|
||||
|
||||
function mdTable(rows, headers) {
|
||||
const header = `| ${headers.join(' | ')} |`;
|
||||
const sep = `| ${headers.map(() => '---').join(' | ')} |`;
|
||||
const body = rows.map((r) => `| ${r.join(' | ')} |`).join('\n');
|
||||
return `${header}\n${sep}\n${body}`;
|
||||
}
|
||||
|
||||
function buildMarkdownReport(largestFiles, byExtensionArr, byDirectoryArr, totalBytes) {
|
||||
const toPct = (num, den) => (den === 0 ? 0 : (num / den) * 100);
|
||||
const md = [];
|
||||
md.push(
|
||||
'\n### Top Largest Files (Top 50)\n',
|
||||
mdTable(
|
||||
largestFiles.map((f) => [
|
||||
f.path,
|
||||
f.sizeFormatted,
|
||||
`${f.percentOfTotal.toFixed(2)}%`,
|
||||
f.ext || '',
|
||||
f.isBinary ? 'binary' : 'text',
|
||||
]),
|
||||
['Path', 'Size', '% of total', 'Ext', 'Type'],
|
||||
),
|
||||
'\n\n### Top Extensions by Bytes (Top 20)\n',
|
||||
);
|
||||
const topExtRows = byExtensionArr
|
||||
.slice(0, 20)
|
||||
.map((e) => [
|
||||
e.ext,
|
||||
String(e.count),
|
||||
formatSize(e.bytes),
|
||||
`${toPct(e.bytes, totalBytes).toFixed(2)}%`,
|
||||
]);
|
||||
md.push(
|
||||
mdTable(topExtRows, ['Ext', 'Count', 'Bytes', '% of total']),
|
||||
'\n\n### Top Directories by Bytes (Top 20)\n',
|
||||
);
|
||||
const topDirRows = byDirectoryArr
|
||||
.slice(0, 20)
|
||||
.map((d) => [
|
||||
d.dir,
|
||||
String(d.count),
|
||||
formatSize(d.bytes),
|
||||
`${toPct(d.bytes, totalBytes).toFixed(2)}%`,
|
||||
]);
|
||||
md.push(mdTable(topDirRows, ['Directory', 'Files', 'Bytes', '% of total']));
|
||||
return md.join('\n');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
KB,
|
||||
MB,
|
||||
formatSize,
|
||||
percentile,
|
||||
processWithLimit,
|
||||
enrichAllFiles,
|
||||
buildHistogram,
|
||||
aggregateByExtension,
|
||||
aggregateByDirectory,
|
||||
computeDepthAndLongest,
|
||||
computeTemporal,
|
||||
computeQuality,
|
||||
computeDuplicates,
|
||||
estimateCompressibility,
|
||||
computeGitInfo,
|
||||
computeLargestFiles,
|
||||
buildMarkdownReport,
|
||||
};
|
||||
@@ -1,79 +1,29 @@
|
||||
const H = require('./stats.helpers.js');
|
||||
|
||||
async function calculateStatistics(aggregatedContent, xmlFileSize, rootDir) {
|
||||
function calculateStatistics(aggregatedContent, xmlFileSize) {
|
||||
const { textFiles, binaryFiles, errors } = aggregatedContent;
|
||||
|
||||
const totalLines = textFiles.reduce((sum, f) => sum + (f.lines || 0), 0);
|
||||
const totalTextSize = textFiles.reduce((sum, file) => sum + file.size, 0);
|
||||
const totalBinarySize = binaryFiles.reduce((sum, file) => sum + file.size, 0);
|
||||
const totalSize = totalTextSize + totalBinarySize;
|
||||
|
||||
const totalLines = textFiles.reduce((sum, file) => sum + file.lines, 0);
|
||||
|
||||
const estimatedTokens = Math.ceil(xmlFileSize / 4);
|
||||
|
||||
// Build enriched file list
|
||||
const allFiles = await H.enrichAllFiles(textFiles, binaryFiles);
|
||||
const totalBytes = allFiles.reduce((s, f) => s + f.size, 0);
|
||||
const sizes = allFiles.map((f) => f.size).sort((a, b) => a - b);
|
||||
const avgSize = sizes.length > 0 ? totalBytes / sizes.length : 0;
|
||||
const medianSize = sizes.length > 0 ? H.percentile(sizes, 50) : 0;
|
||||
const p90 = H.percentile(sizes, 90);
|
||||
const p95 = H.percentile(sizes, 95);
|
||||
const p99 = H.percentile(sizes, 99);
|
||||
|
||||
const histogram = H.buildHistogram(allFiles);
|
||||
const byExtensionArr = H.aggregateByExtension(allFiles);
|
||||
const byDirectoryArr = H.aggregateByDirectory(allFiles);
|
||||
const { depthDist, longestPaths } = H.computeDepthAndLongest(allFiles);
|
||||
const temporal = H.computeTemporal(allFiles, Date.now());
|
||||
const quality = H.computeQuality(allFiles, textFiles);
|
||||
const duplicateCandidates = H.computeDuplicates(allFiles, textFiles);
|
||||
const compressibilityRatio = H.estimateCompressibility(textFiles);
|
||||
const git = H.computeGitInfo(allFiles, rootDir, quality.largeThreshold);
|
||||
const largestFiles = H.computeLargestFiles(allFiles, totalBytes);
|
||||
const markdownReport = H.buildMarkdownReport(
|
||||
largestFiles,
|
||||
byExtensionArr,
|
||||
byDirectoryArr,
|
||||
totalBytes,
|
||||
);
|
||||
const formatSize = (bytes) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
return {
|
||||
// Back-compat summary
|
||||
totalFiles: textFiles.length + binaryFiles.length,
|
||||
textFiles: textFiles.length,
|
||||
binaryFiles: binaryFiles.length,
|
||||
errorFiles: errors.length,
|
||||
totalSize: H.formatSize(totalBytes),
|
||||
totalBytes,
|
||||
xmlSize: H.formatSize(xmlFileSize),
|
||||
totalSize: formatSize(totalSize),
|
||||
xmlSize: formatSize(xmlFileSize),
|
||||
totalLines,
|
||||
estimatedTokens: estimatedTokens.toLocaleString(),
|
||||
|
||||
// Distributions and percentiles
|
||||
avgFileSize: avgSize,
|
||||
medianFileSize: medianSize,
|
||||
p90,
|
||||
p95,
|
||||
p99,
|
||||
histogram,
|
||||
|
||||
// Extensions and directories
|
||||
byExtension: byExtensionArr,
|
||||
byDirectory: byDirectoryArr,
|
||||
depthDistribution: depthDist,
|
||||
longestPaths,
|
||||
|
||||
// Temporal
|
||||
temporal,
|
||||
|
||||
// Quality signals
|
||||
quality,
|
||||
|
||||
// Duplicates and compressibility
|
||||
duplicateCandidates,
|
||||
compressibilityRatio,
|
||||
|
||||
// Git-aware
|
||||
git,
|
||||
|
||||
largestFiles,
|
||||
markdownReport,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,413 +0,0 @@
|
||||
/* deno-lint-ignore-file */
|
||||
/*
|
||||
Automatic test matrix for project root detection.
|
||||
Creates temporary fixtures for various ecosystems and validates findProjectRoot().
|
||||
No external options or flags required. Safe to run multiple times.
|
||||
*/
|
||||
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { promisify } = require('node:util');
|
||||
const { execFile } = require('node:child_process');
|
||||
const process = require('node:process');
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const { findProjectRoot } = require('./projectRoot.js');
|
||||
|
||||
async function cmdAvailable(cmd) {
|
||||
try {
|
||||
await execFileAsync(cmd, ['--version'], { timeout: 500, windowsHide: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function testSvnMarker() {
|
||||
const root = await mkTmpDir('svn');
|
||||
const nested = path.join(root, 'proj', 'code');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.ensureDir(path.join(root, '.svn'));
|
||||
const found = await findProjectRoot(nested);
|
||||
assertEqual(found, root, '.svn marker should be detected');
|
||||
return { name: 'svn-marker', ok: true };
|
||||
}
|
||||
|
||||
async function testSymlinkStart() {
|
||||
const root = await mkTmpDir('symlink-start');
|
||||
const nested = path.join(root, 'a', 'b');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, '.project-root'), '\n');
|
||||
const tmp = await mkTmpDir('symlink-tmp');
|
||||
const link = path.join(tmp, 'link-to-b');
|
||||
try {
|
||||
await fs.symlink(nested, link);
|
||||
} catch {
|
||||
// symlink may not be permitted on some systems; skip
|
||||
return { name: 'symlink-start', ok: true, skipped: true };
|
||||
}
|
||||
const found = await findProjectRoot(link);
|
||||
assertEqual(found, root, 'should resolve symlinked start to real root');
|
||||
return { name: 'symlink-start', ok: true };
|
||||
}
|
||||
|
||||
async function testSubmoduleLikeInnerGitFile() {
|
||||
const root = await mkTmpDir('submodule-like');
|
||||
const mid = path.join(root, 'mid');
|
||||
const leaf = path.join(mid, 'leaf');
|
||||
await fs.ensureDir(leaf);
|
||||
// outer repo
|
||||
await fs.ensureDir(path.join(root, '.git'));
|
||||
// inner submodule-like .git file
|
||||
await fs.writeFile(path.join(mid, '.git'), 'gitdir: ../.git/modules/mid\n');
|
||||
const found = await findProjectRoot(leaf);
|
||||
assertEqual(found, root, 'outermost .git should win on tie weight');
|
||||
return { name: 'submodule-like-gitfile', ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
async function mkTmpDir(name) {
|
||||
const base = await fs.realpath(os.tmpdir());
|
||||
const dir = await fs.mkdtemp(path.join(base, `flattener-${name}-`));
|
||||
return dir;
|
||||
}
|
||||
|
||||
function assertEqual(actual, expected, msg) {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${msg}: expected="${expected}" actual="${actual}"`);
|
||||
}
|
||||
}
|
||||
|
||||
async function testSentinel() {
|
||||
const root = await mkTmpDir('sentinel');
|
||||
const nested = path.join(root, 'a', 'b', 'c');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, '.project-root'), '\n');
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'sentinel .project-root should win');
|
||||
return { name: 'sentinel', ok: true };
|
||||
}
|
||||
|
||||
async function testOtherSentinels() {
|
||||
const root = await mkTmpDir('other-sentinels');
|
||||
const nested = path.join(root, 'x', 'y');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, '.workspace-root'), '\n');
|
||||
const found1 = await findProjectRoot(nested);
|
||||
assertEqual(found1, root, 'sentinel .workspace-root should win');
|
||||
|
||||
await fs.remove(path.join(root, '.workspace-root'));
|
||||
await fs.writeFile(path.join(root, '.repo-root'), '\n');
|
||||
const found2 = await findProjectRoot(nested);
|
||||
assertEqual(found2, root, 'sentinel .repo-root should win');
|
||||
return { name: 'other-sentinels', ok: true };
|
||||
}
|
||||
|
||||
async function testGitCliAndMarker() {
|
||||
const hasGit = await cmdAvailable('git');
|
||||
if (!hasGit) return { name: 'git-cli', ok: true, skipped: true };
|
||||
|
||||
const root = await mkTmpDir('git');
|
||||
const nested = path.join(root, 'pkg', 'src');
|
||||
await fs.ensureDir(nested);
|
||||
await execFileAsync('git', ['init'], { cwd: root, timeout: 2000 });
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'git toplevel should be detected');
|
||||
return { name: 'git-cli', ok: true };
|
||||
}
|
||||
|
||||
async function testHgMarkerOrCli() {
|
||||
// Prefer simple marker test to avoid requiring Mercurial install
|
||||
const root = await mkTmpDir('hg');
|
||||
const nested = path.join(root, 'lib');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.ensureDir(path.join(root, '.hg'));
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, '.hg marker should be detected');
|
||||
return { name: 'hg-marker', ok: true };
|
||||
}
|
||||
|
||||
async function testWorkspacePnpm() {
|
||||
const root = await mkTmpDir('pnpm-workspace');
|
||||
const pkgA = path.join(root, 'packages', 'a');
|
||||
await fs.ensureDir(pkgA);
|
||||
await fs.writeFile(path.join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n');
|
||||
const found = await findProjectRoot(pkgA);
|
||||
await assertEqual(found, root, 'pnpm-workspace.yaml should be detected');
|
||||
return { name: 'pnpm-workspace', ok: true };
|
||||
}
|
||||
|
||||
async function testPackageJsonWorkspaces() {
|
||||
const root = await mkTmpDir('package-workspaces');
|
||||
const pkgA = path.join(root, 'packages', 'a');
|
||||
await fs.ensureDir(pkgA);
|
||||
await fs.writeJson(
|
||||
path.join(root, 'package.json'),
|
||||
{ private: true, workspaces: ['packages/*'] },
|
||||
{ spaces: 2 },
|
||||
);
|
||||
const found = await findProjectRoot(pkgA);
|
||||
await assertEqual(found, root, 'package.json workspaces should be detected');
|
||||
return { name: 'package.json-workspaces', ok: true };
|
||||
}
|
||||
|
||||
async function testLockfiles() {
|
||||
const root = await mkTmpDir('lockfiles');
|
||||
const nested = path.join(root, 'src');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, 'yarn.lock'), '\n');
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'yarn.lock should be detected');
|
||||
return { name: 'lockfiles', ok: true };
|
||||
}
|
||||
|
||||
async function testLanguageConfigs() {
|
||||
const root = await mkTmpDir('lang-configs');
|
||||
const nested = path.join(root, 'x', 'y');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, 'pyproject.toml'), "[tool.poetry]\nname='tmp'\n");
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'pyproject.toml should be detected');
|
||||
return { name: 'language-configs', ok: true };
|
||||
}
|
||||
|
||||
async function testPreferOuterOnTie() {
|
||||
const root = await mkTmpDir('tie');
|
||||
const mid = path.join(root, 'mid');
|
||||
const leaf = path.join(mid, 'leaf');
|
||||
await fs.ensureDir(leaf);
|
||||
// same weight marker at two levels
|
||||
await fs.writeFile(path.join(root, 'requirements.txt'), '\n');
|
||||
await fs.writeFile(path.join(mid, 'requirements.txt'), '\n');
|
||||
const found = await findProjectRoot(leaf);
|
||||
await assertEqual(found, root, 'outermost directory should win on equal weight');
|
||||
return { name: 'prefer-outermost-tie', ok: true };
|
||||
}
|
||||
|
||||
// Additional coverage: Bazel, Nx/Turbo/Rush, Go workspaces, Deno, Java/Scala, PHP, Rust, Nix, Changesets, env markers,
|
||||
// and priority interaction between package.json and lockfiles.
|
||||
|
||||
async function testBazelWorkspace() {
|
||||
const root = await mkTmpDir('bazel');
|
||||
const nested = path.join(root, 'apps', 'svc');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, 'WORKSPACE'), 'workspace(name="tmp")\n');
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'Bazel WORKSPACE should be detected');
|
||||
return { name: 'bazel-workspace', ok: true };
|
||||
}
|
||||
|
||||
async function testNx() {
|
||||
const root = await mkTmpDir('nx');
|
||||
const nested = path.join(root, 'apps', 'web');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeJson(path.join(root, 'nx.json'), { npmScope: 'tmp' }, { spaces: 2 });
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'nx.json should be detected');
|
||||
return { name: 'nx', ok: true };
|
||||
}
|
||||
|
||||
async function testTurbo() {
|
||||
const root = await mkTmpDir('turbo');
|
||||
const nested = path.join(root, 'packages', 'x');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeJson(path.join(root, 'turbo.json'), { pipeline: {} }, { spaces: 2 });
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'turbo.json should be detected');
|
||||
return { name: 'turbo', ok: true };
|
||||
}
|
||||
|
||||
async function testRush() {
|
||||
const root = await mkTmpDir('rush');
|
||||
const nested = path.join(root, 'apps', 'a');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeJson(path.join(root, 'rush.json'), { projectFolderMinDepth: 1 }, { spaces: 2 });
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'rush.json should be detected');
|
||||
return { name: 'rush', ok: true };
|
||||
}
|
||||
|
||||
async function testGoWorkAndMod() {
|
||||
const root = await mkTmpDir('gowork');
|
||||
const mod = path.join(root, 'modA');
|
||||
const nested = path.join(mod, 'pkg');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, 'go.work'), 'go 1.22\nuse ./modA\n');
|
||||
await fs.writeFile(path.join(mod, 'go.mod'), 'module example.com/a\ngo 1.22\n');
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'go.work should define the workspace root');
|
||||
return { name: 'go-work', ok: true };
|
||||
}
|
||||
|
||||
async function testDenoJson() {
|
||||
const root = await mkTmpDir('deno');
|
||||
const nested = path.join(root, 'src');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeJson(path.join(root, 'deno.json'), { tasks: {} }, { spaces: 2 });
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'deno.json should be detected');
|
||||
return { name: 'deno-json', ok: true };
|
||||
}
|
||||
|
||||
async function testGradleSettings() {
|
||||
const root = await mkTmpDir('gradle');
|
||||
const nested = path.join(root, 'app');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, 'settings.gradle'), "rootProject.name='tmp'\n");
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'settings.gradle should be detected');
|
||||
return { name: 'gradle-settings', ok: true };
|
||||
}
|
||||
|
||||
async function testMavenPom() {
|
||||
const root = await mkTmpDir('maven');
|
||||
const nested = path.join(root, 'module');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, 'pom.xml'), '<project></project>\n');
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'pom.xml should be detected');
|
||||
return { name: 'maven-pom', ok: true };
|
||||
}
|
||||
|
||||
async function testSbtBuild() {
|
||||
const root = await mkTmpDir('sbt');
|
||||
const nested = path.join(root, 'sub');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, 'build.sbt'), 'name := "tmp"\n');
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'build.sbt should be detected');
|
||||
return { name: 'sbt-build', ok: true };
|
||||
}
|
||||
|
||||
async function testComposer() {
|
||||
const root = await mkTmpDir('composer');
|
||||
const nested = path.join(root, 'src');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeJson(path.join(root, 'composer.json'), { name: 'tmp/pkg' }, { spaces: 2 });
|
||||
await fs.writeFile(path.join(root, 'composer.lock'), '{}\n');
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'composer.{json,lock} should be detected');
|
||||
return { name: 'composer', ok: true };
|
||||
}
|
||||
|
||||
async function testCargo() {
|
||||
const root = await mkTmpDir('cargo');
|
||||
const nested = path.join(root, 'src');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, 'Cargo.toml'), "[package]\nname='tmp'\nversion='0.0.0'\n");
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'Cargo.toml should be detected');
|
||||
return { name: 'cargo', ok: true };
|
||||
}
|
||||
|
||||
async function testNixFlake() {
|
||||
const root = await mkTmpDir('nix');
|
||||
const nested = path.join(root, 'work');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, 'flake.nix'), '{ }\n');
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'flake.nix should be detected');
|
||||
return { name: 'nix-flake', ok: true };
|
||||
}
|
||||
|
||||
async function testChangesetConfig() {
|
||||
const root = await mkTmpDir('changeset');
|
||||
const nested = path.join(root, 'pkg');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.ensureDir(path.join(root, '.changeset'));
|
||||
await fs.writeJson(
|
||||
path.join(root, '.changeset', 'config.json'),
|
||||
{ $schema: 'https://unpkg.com/@changesets/config@2.3.1/schema.json' },
|
||||
{ spaces: 2 },
|
||||
);
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, '.changeset/config.json should be detected');
|
||||
return { name: 'changesets', ok: true };
|
||||
}
|
||||
|
||||
async function testEnvCustomMarker() {
|
||||
const root = await mkTmpDir('env-marker');
|
||||
const nested = path.join(root, 'dir');
|
||||
await fs.ensureDir(nested);
|
||||
await fs.writeFile(path.join(root, 'MY_ROOT'), '\n');
|
||||
const prev = process.env.PROJECT_ROOT_MARKERS;
|
||||
process.env.PROJECT_ROOT_MARKERS = 'MY_ROOT';
|
||||
try {
|
||||
const found = await findProjectRoot(nested);
|
||||
await assertEqual(found, root, 'custom env marker should be honored');
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.PROJECT_ROOT_MARKERS;
|
||||
else process.env.PROJECT_ROOT_MARKERS = prev;
|
||||
}
|
||||
return { name: 'env-custom-marker', ok: true };
|
||||
}
|
||||
|
||||
async function testPackageLowPriorityVsLock() {
|
||||
const root = await mkTmpDir('pkg-vs-lock');
|
||||
const nested = path.join(root, 'nested');
|
||||
await fs.ensureDir(path.join(nested, 'deep'));
|
||||
await fs.writeJson(path.join(nested, 'package.json'), { name: 'nested' }, { spaces: 2 });
|
||||
await fs.writeFile(path.join(root, 'yarn.lock'), '\n');
|
||||
const found = await findProjectRoot(path.join(nested, 'deep'));
|
||||
await assertEqual(found, root, 'lockfile at root should outrank nested package.json');
|
||||
return { name: 'package-vs-lock-priority', ok: true };
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const tests = [
|
||||
testSentinel,
|
||||
testOtherSentinels,
|
||||
testGitCliAndMarker,
|
||||
testHgMarkerOrCli,
|
||||
testWorkspacePnpm,
|
||||
testPackageJsonWorkspaces,
|
||||
testLockfiles,
|
||||
testLanguageConfigs,
|
||||
testPreferOuterOnTie,
|
||||
testBazelWorkspace,
|
||||
testNx,
|
||||
testTurbo,
|
||||
testRush,
|
||||
testGoWorkAndMod,
|
||||
testDenoJson,
|
||||
testGradleSettings,
|
||||
testMavenPom,
|
||||
testSbtBuild,
|
||||
testComposer,
|
||||
testCargo,
|
||||
testNixFlake,
|
||||
testChangesetConfig,
|
||||
testEnvCustomMarker,
|
||||
testPackageLowPriorityVsLock,
|
||||
testSvnMarker,
|
||||
testSymlinkStart,
|
||||
testSubmoduleLikeInnerGitFile,
|
||||
];
|
||||
|
||||
const results = [];
|
||||
for (const t of tests) {
|
||||
try {
|
||||
const r = await t();
|
||||
results.push({ ...r, ok: true });
|
||||
console.log(`✔ ${r.name}${r.skipped ? ' (skipped)' : ''}`);
|
||||
} catch (error) {
|
||||
console.error(`✖ ${t.name}:`, error && error.message ? error.message : error);
|
||||
results.push({ name: t.name, ok: false, error: String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
const failed = results.filter((r) => !r.ok);
|
||||
console.log('\nSummary:');
|
||||
for (const r of results) {
|
||||
console.log(`- ${r.name}: ${r.ok ? 'ok' : 'FAIL'}${r.skipped ? ' (skipped)' : ''}`);
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,44 +1,49 @@
|
||||
const fs = require('fs-extra');
|
||||
const fs = require("fs-extra");
|
||||
|
||||
function escapeXml(string_) {
|
||||
if (typeof string_ !== 'string') {
|
||||
return String(string_);
|
||||
function escapeXml(str) {
|
||||
if (typeof str !== "string") {
|
||||
return String(str);
|
||||
}
|
||||
return string_.replaceAll('&', '&').replaceAll('<', '<').replaceAll("'", ''');
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function indentFileContent(content) {
|
||||
if (typeof content !== 'string') {
|
||||
if (typeof content !== "string") {
|
||||
return String(content);
|
||||
}
|
||||
return content.split('\n').map((line) => ` ${line}`);
|
||||
return content.split("\n").map((line) => ` ${line}`);
|
||||
}
|
||||
|
||||
function generateXMLOutput(aggregatedContent, outputPath) {
|
||||
const { textFiles } = aggregatedContent;
|
||||
const writeStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
|
||||
const writeStream = fs.createWriteStream(outputPath, { encoding: "utf8" });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writeStream.on('error', reject);
|
||||
writeStream.on('finish', resolve);
|
||||
writeStream.on("error", reject);
|
||||
writeStream.on("finish", resolve);
|
||||
|
||||
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
|
||||
const filesSorted = [...textFiles].sort((a, b) => a.path.localeCompare(b.path));
|
||||
const filesSorted = [...textFiles].sort((a, b) =>
|
||||
a.path.localeCompare(b.path)
|
||||
);
|
||||
let index = 0;
|
||||
|
||||
const writeNext = () => {
|
||||
if (index >= filesSorted.length) {
|
||||
writeStream.write('</files>\n');
|
||||
writeStream.write("</files>\n");
|
||||
writeStream.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const file = filesSorted[index++];
|
||||
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) {
|
||||
writeStream.write(`\t<file path='${p}'/>\n`);
|
||||
@@ -46,34 +51,27 @@ function generateXMLOutput(aggregatedContent, outputPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const needsCdata = content.includes('<') || content.includes('&') || content.includes(']]>');
|
||||
const needsCdata = content.includes("<") || content.includes("&") ||
|
||||
content.includes("]]>");
|
||||
if (needsCdata) {
|
||||
// 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`);
|
||||
// Safely split any occurrences of "]]>" inside content, trim trailing newlines, indent each line with two tabs
|
||||
const safe = content.replaceAll(']]>', ']]]]><![CDATA[>');
|
||||
const trimmed = safe.replace(/[\r\n]+$/, '');
|
||||
const indented =
|
||||
trimmed.length > 0
|
||||
? trimmed
|
||||
.split('\n')
|
||||
.map((line) => `\t\t${line}`)
|
||||
.join('\n')
|
||||
: '';
|
||||
const safe = content.replace(/]]>/g, "]]]]><![CDATA[>");
|
||||
const trimmed = safe.replace(/[\r\n]+$/, "");
|
||||
const indented = trimmed.length > 0
|
||||
? trimmed.split("\n").map((line) => `\t\t${line}`).join("\n")
|
||||
: "";
|
||||
writeStream.write(indented);
|
||||
// Close CDATA and attach closing tag directly after the last content line
|
||||
writeStream.write(']]></file>\n');
|
||||
writeStream.write("]]></file>\n");
|
||||
} else {
|
||||
// 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`);
|
||||
const trimmed = content.replace(/[\r\n]+$/, '');
|
||||
const indented =
|
||||
trimmed.length > 0
|
||||
? trimmed
|
||||
.split('\n')
|
||||
.map((line) => `\t\t${line}`)
|
||||
.join('\n')
|
||||
: '';
|
||||
const trimmed = content.replace(/[\r\n]+$/, "");
|
||||
const indented = trimmed.length > 0
|
||||
? trimmed.split("\n").map((line) => `\t\t${line}`).join("\n")
|
||||
: "";
|
||||
writeStream.write(indented);
|
||||
writeStream.write(`</file>\n`);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { program } = require('commander');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs').promises;
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const yaml = require('js-yaml');
|
||||
const chalk = require('chalk').default || require('chalk');
|
||||
const inquirer = require('inquirer').default || require('inquirer');
|
||||
const semver = require('semver');
|
||||
const https = require('node:https');
|
||||
const https = require('https');
|
||||
|
||||
// Handle both execution contexts (from root via npx or from installer directory)
|
||||
let version;
|
||||
@@ -18,20 +18,18 @@ try {
|
||||
version = require('../package.json').version;
|
||||
packageName = require('../package.json').name;
|
||||
installer = require('../lib/installer');
|
||||
} catch (error) {
|
||||
} catch (e) {
|
||||
// Fall back to root context (when run via npx from GitHub)
|
||||
console.log(`Installer context not found (${error.message}), trying root context...`);
|
||||
console.log(`Installer context not found (${e.message}), trying root context...`);
|
||||
try {
|
||||
version = require('../../../package.json').version;
|
||||
installer = require('../../../tools/installer/lib/installer');
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error: Could not load required modules. Please ensure you are running from the correct directory.',
|
||||
);
|
||||
} catch (e2) {
|
||||
console.error('Error: Could not load required modules. Please ensure you are running from the correct directory.');
|
||||
console.error('Debug info:', {
|
||||
__dirname,
|
||||
cwd: process.cwd(),
|
||||
error: error.message,
|
||||
error: e2.message
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -47,14 +45,8 @@ program
|
||||
.option('-f, --full', 'Install complete BMad Method')
|
||||
.option('-x, --expansion-only', 'Install only expansion packs (no bmad-core)')
|
||||
.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(
|
||||
'-e, --expansion-packs <packs...>',
|
||||
'Install specific expansion packs (can specify multiple)',
|
||||
)
|
||||
.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('-e, --expansion-packs <packs...>', 'Install specific expansion packs (can specify multiple)')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
if (!options.full && !options.expansionOnly) {
|
||||
@@ -72,8 +64,8 @@ program
|
||||
const config = {
|
||||
installType,
|
||||
directory: options.directory || '.',
|
||||
ides: (options.ide || []).filter((ide) => ide !== 'other'),
|
||||
expansionPacks: options.expansionPacks || [],
|
||||
ides: (options.ide || []).filter(ide => ide !== 'other'),
|
||||
expansionPacks: options.expansionPacks || []
|
||||
};
|
||||
await installer.install(config);
|
||||
process.exit(0);
|
||||
@@ -106,7 +98,7 @@ program
|
||||
console.log('Checking for updates...');
|
||||
|
||||
// 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)
|
||||
if (res.statusCode !== 200) {
|
||||
console.error(chalk.red(`Update check failed: Received status code ${res.statusCode}`));
|
||||
@@ -115,7 +107,7 @@ program
|
||||
|
||||
// Accumulate response data chunks
|
||||
let data = '';
|
||||
res.on('data', (chunk) => (data += chunk));
|
||||
res.on('data', chunk => data += chunk);
|
||||
|
||||
// Process complete response
|
||||
res.on('end', () => {
|
||||
@@ -125,9 +117,7 @@ program
|
||||
|
||||
// Compare versions using semver
|
||||
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.magenta(` npm install ${packageName}@latest`));
|
||||
console.log(chalk.dim(' or'));
|
||||
@@ -143,12 +133,12 @@ program
|
||||
});
|
||||
|
||||
// Handle network/connection errors
|
||||
req.on('error', (error) => {
|
||||
req.on('error', error => {
|
||||
console.error(chalk.red('Update check failed:'), error.message);
|
||||
});
|
||||
|
||||
// Set 30 second timeout to prevent hanging
|
||||
req.setTimeout(30_000, () => {
|
||||
req.setTimeout(30000, () => {
|
||||
req.destroy();
|
||||
console.error(chalk.red('Update check timed out'));
|
||||
});
|
||||
@@ -193,17 +183,16 @@ program
|
||||
});
|
||||
|
||||
async function promptInstallation() {
|
||||
|
||||
// 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.blue(`✨ Installer v${version}\n`));
|
||||
@@ -221,8 +210,8 @@ async function promptInstallation() {
|
||||
return 'Please enter a valid project path';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
]);
|
||||
answers.directory = directory;
|
||||
|
||||
@@ -249,10 +238,9 @@ async function promptInstallation() {
|
||||
if (state.type === 'v4_existing') {
|
||||
const currentVersion = state.manifest?.version || 'unknown';
|
||||
const newVersion = version; // Always use package.json version
|
||||
const versionInfo =
|
||||
currentVersion === newVersion
|
||||
? `(v${currentVersion} - reinstall)`
|
||||
: `(v${currentVersion} → v${newVersion})`;
|
||||
const versionInfo = currentVersion === newVersion
|
||||
? `(v${currentVersion} - reinstall)`
|
||||
: `(v${currentVersion} → v${newVersion})`;
|
||||
bmadOptionText = `Update ${coreShortTitle} ${versionInfo} .bmad-core`;
|
||||
} else {
|
||||
bmadOptionText = `${coreShortTitle} (v${version}) .bmad-core`;
|
||||
@@ -261,7 +249,7 @@ async function promptInstallation() {
|
||||
choices.push({
|
||||
name: bmadOptionText,
|
||||
value: 'bmad-core',
|
||||
checked: true,
|
||||
checked: true
|
||||
});
|
||||
|
||||
// Add expansion pack options
|
||||
@@ -272,10 +260,9 @@ async function promptInstallation() {
|
||||
if (existing) {
|
||||
const currentVersion = existing.manifest?.version || 'unknown';
|
||||
const newVersion = pack.version;
|
||||
const versionInfo =
|
||||
currentVersion === newVersion
|
||||
? `(v${currentVersion} - reinstall)`
|
||||
: `(v${currentVersion} → v${newVersion})`;
|
||||
const versionInfo = currentVersion === newVersion
|
||||
? `(v${currentVersion} - reinstall)`
|
||||
: `(v${currentVersion} → v${newVersion})`;
|
||||
packOptionText = `Update ${pack.shortTitle} ${versionInfo} .${pack.id}`;
|
||||
} else {
|
||||
packOptionText = `${pack.shortTitle} (v${pack.version}) .${pack.id}`;
|
||||
@@ -284,7 +271,7 @@ async function promptInstallation() {
|
||||
choices.push({
|
||||
name: packOptionText,
|
||||
value: pack.id,
|
||||
checked: false,
|
||||
checked: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -300,13 +287,13 @@ async function promptInstallation() {
|
||||
return 'Please select at least one item to install';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
// Process selections
|
||||
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
|
||||
if (selectedItems.includes('bmad-core')) {
|
||||
@@ -319,8 +306,8 @@ async function promptInstallation() {
|
||||
type: 'confirm',
|
||||
name: 'prdSharded',
|
||||
message: 'Will the PRD (Product Requirements Document) be sharded into multiple files?',
|
||||
default: true,
|
||||
},
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
answers.prdSharded = prdSharded;
|
||||
|
||||
@@ -330,30 +317,18 @@ async function promptInstallation() {
|
||||
type: 'confirm',
|
||||
name: 'architectureSharded',
|
||||
message: 'Will the architecture documentation be sharded into multiple files?',
|
||||
default: true,
|
||||
},
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
answers.architectureSharded = architectureSharded;
|
||||
|
||||
// Show warning if architecture sharding is disabled
|
||||
if (!architectureSharded) {
|
||||
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(
|
||||
chalk.yellow(
|
||||
'in devLoadAlwaysFiles (like coding-standards.md, tech-stack.md, source-tree.md)',
|
||||
),
|
||||
);
|
||||
console.log(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(
|
||||
'\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.'));
|
||||
|
||||
const { acknowledge } = await inquirer.prompt([
|
||||
@@ -361,8 +336,8 @@ async function promptInstallation() {
|
||||
type: 'confirm',
|
||||
name: 'acknowledge',
|
||||
message: 'Do you acknowledge this requirement and want to proceed?',
|
||||
default: false,
|
||||
},
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (!acknowledge) {
|
||||
@@ -378,11 +353,7 @@ async function promptInstallation() {
|
||||
|
||||
while (!ideSelectionComplete) {
|
||||
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 SPACEBAR to select/deselect IDEs'));
|
||||
console.log(chalk.bold.magenta('🔸 Press ENTER when finished selecting\n'));
|
||||
@@ -391,8 +362,7 @@ async function promptInstallation() {
|
||||
{
|
||||
type: 'checkbox',
|
||||
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: [
|
||||
{ name: 'Cursor', value: 'cursor' },
|
||||
{ name: 'Claude Code', value: 'claude-code' },
|
||||
@@ -403,10 +373,9 @@ async function promptInstallation() {
|
||||
{ name: 'Cline', value: 'cline' },
|
||||
{ name: 'Gemini CLI', value: 'gemini' },
|
||||
{ name: 'Qwen Code', value: 'qwen-code' },
|
||||
{ name: 'Crush', value: 'crush' },
|
||||
{ name: 'Github Copilot', value: 'github-copilot' },
|
||||
],
|
||||
},
|
||||
{ name: 'Github Copilot', value: 'github-copilot' }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
ides = ideResponse.ides;
|
||||
@@ -417,19 +386,13 @@ async function promptInstallation() {
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirmNoIde',
|
||||
message: chalk.red(
|
||||
'⚠️ You have NOT selected any IDEs. This means NO IDE integration will be set up. Is this correct?',
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
message: chalk.red('⚠️ You have NOT selected any IDEs. This means NO IDE integration will be set up. Is this correct?'),
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -443,9 +406,7 @@ async function promptInstallation() {
|
||||
// Configure GitHub Copilot immediately if selected
|
||||
if (ides.includes('github-copilot')) {
|
||||
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([
|
||||
{
|
||||
@@ -455,19 +416,19 @@ async function promptInstallation() {
|
||||
choices: [
|
||||
{
|
||||
name: 'Use recommended defaults (fastest setup)',
|
||||
value: 'defaults',
|
||||
value: 'defaults'
|
||||
},
|
||||
{
|
||||
name: 'Configure each setting manually (customize to your preferences)',
|
||||
value: 'manual',
|
||||
value: 'manual'
|
||||
},
|
||||
{
|
||||
name: "Skip settings configuration (I'll configure manually later)",
|
||||
value: 'skip',
|
||||
},
|
||||
name: 'Skip settings configuration (I\'ll configure manually later)',
|
||||
value: 'skip'
|
||||
}
|
||||
],
|
||||
default: 'defaults',
|
||||
},
|
||||
default: 'defaults'
|
||||
}
|
||||
]);
|
||||
|
||||
answers.githubCopilotConfig = { configChoice };
|
||||
@@ -478,17 +439,14 @@ async function promptInstallation() {
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'includeWebBundles',
|
||||
message:
|
||||
'Would you like to include pre-built web bundles? (standalone files for ChatGPT, Claude, Gemini)',
|
||||
default: false,
|
||||
},
|
||||
message: 'Would you like to include pre-built web bundles? (standalone files for ChatGPT, Claude, Gemini)',
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (includeWebBundles) {
|
||||
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([
|
||||
{
|
||||
@@ -498,22 +456,22 @@ async function promptInstallation() {
|
||||
choices: [
|
||||
{
|
||||
name: 'All available bundles (agents, teams, expansion packs)',
|
||||
value: 'all',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
name: 'Specific teams only',
|
||||
value: 'teams',
|
||||
value: 'teams'
|
||||
},
|
||||
{
|
||||
name: 'Individual agents only',
|
||||
value: 'agents',
|
||||
value: 'agents'
|
||||
},
|
||||
{
|
||||
name: 'Custom selection',
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
},
|
||||
value: 'custom'
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
answers.webBundleType = webBundleType;
|
||||
@@ -526,18 +484,18 @@ async function promptInstallation() {
|
||||
type: 'checkbox',
|
||||
name: 'selectedTeams',
|
||||
message: 'Select team bundles to include:',
|
||||
choices: teams.map((t) => ({
|
||||
choices: teams.map(t => ({
|
||||
name: `${t.icon || '📋'} ${t.name}: ${t.description}`,
|
||||
value: t.id,
|
||||
checked: webBundleType === 'teams', // Check all if teams-only mode
|
||||
checked: webBundleType === 'teams' // Check all if teams-only mode
|
||||
})),
|
||||
validate: (answer) => {
|
||||
if (answer.length === 0) {
|
||||
if (answer.length < 1) {
|
||||
return 'You must select at least one team.';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
]);
|
||||
answers.selectedWebBundleTeams = selectedTeams;
|
||||
}
|
||||
@@ -549,8 +507,8 @@ async function promptInstallation() {
|
||||
type: 'confirm',
|
||||
name: 'includeIndividualAgents',
|
||||
message: 'Also include individual agent bundles?',
|
||||
default: true,
|
||||
},
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
answers.includeIndividualAgents = includeIndividualAgents;
|
||||
}
|
||||
@@ -566,8 +524,8 @@ async function promptInstallation() {
|
||||
return 'Please enter a valid directory path';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
]);
|
||||
answers.webBundlesDirectory = webBundlesDirectory;
|
||||
}
|
||||
@@ -580,6 +538,6 @@ async function promptInstallation() {
|
||||
program.parse(process.argv);
|
||||
|
||||
// Show help if no command provided
|
||||
if (process.argv.slice(2).length === 0) {
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
}
|
||||
@@ -28,24 +28,14 @@ ide-configurations:
|
||||
# To use BMad agents in Claude Code:
|
||||
# 1. Type /agent-name (e.g., "/dev", "/pm", "/architect")
|
||||
# 2. Claude will switch to that agent's persona
|
||||
crush:
|
||||
name: Crush
|
||||
rule-dir: .crush/commands/BMad/
|
||||
format: multi-file
|
||||
command-suffix: .md
|
||||
instructions: |
|
||||
# To use BMad agents in Crush:
|
||||
# 1. Press CTRL + P and press TAB
|
||||
# 2. Select agent or task
|
||||
# 3. Crush will switch to that agent's persona / task
|
||||
windsurf:
|
||||
name: Windsurf
|
||||
rule-dir: .windsurf/workflows/
|
||||
rule-dir: .windsurf/rules/
|
||||
format: multi-file
|
||||
command-suffix: .md
|
||||
instructions: |
|
||||
# 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
|
||||
trae:
|
||||
name: Trae
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const { extractYamlFromAgent } = require('../../lib/yaml-utils');
|
||||
|
||||
@@ -51,7 +51,7 @@ class ConfigLoader {
|
||||
id: agentId,
|
||||
name: agentConfig.title || agentConfig.name || agentId,
|
||||
file: `bmad-core/agents/${entry.name}`,
|
||||
description: agentConfig.whenToUse || 'No description available',
|
||||
description: agentConfig.whenToUse || 'No description available'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -90,25 +90,21 @@ class ConfigLoader {
|
||||
expansionPacks.push({
|
||||
id: entry.name,
|
||||
name: config.name || entry.name,
|
||||
description:
|
||||
config['short-title'] || config.description || 'No description available',
|
||||
fullDescription:
|
||||
config.description || config['short-title'] || 'No description available',
|
||||
description: config['short-title'] || config.description || 'No description available',
|
||||
fullDescription: config.description || config['short-title'] || 'No description available',
|
||||
version: config.version || '1.0.0',
|
||||
author: config.author || 'BMad Team',
|
||||
packPath: packPath,
|
||||
dependencies: config.dependencies?.agents || [],
|
||||
dependencies: config.dependencies?.agents || []
|
||||
});
|
||||
} catch (error) {
|
||||
// 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
|
||||
const name = entry.name
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
|
||||
expansionPacks.push({
|
||||
@@ -119,7 +115,7 @@ class ConfigLoader {
|
||||
version: '1.0.0',
|
||||
author: 'BMad Team',
|
||||
packPath: packPath,
|
||||
dependencies: [],
|
||||
dependencies: []
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -197,7 +193,7 @@ class ConfigLoader {
|
||||
id: path.basename(entry.name, '.yaml'),
|
||||
name: teamConfig.bundle.name || entry.name,
|
||||
description: teamConfig.bundle.description || 'Team configuration',
|
||||
icon: teamConfig.bundle.icon || '📋',
|
||||
icon: teamConfig.bundle.icon || '📋'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const yaml = require('js-yaml');
|
||||
const chalk = require('chalk');
|
||||
const { createReadStream, createWriteStream, promises: fsPromises } = require('node:fs');
|
||||
const { pipeline } = require('node:stream/promises');
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const yaml = require("js-yaml");
|
||||
const chalk = require("chalk").default || require("chalk");
|
||||
const { createReadStream, createWriteStream, promises: fsPromises } = require('fs');
|
||||
const { pipeline } = require('stream/promises');
|
||||
const resourceLocator = require('./resource-locator');
|
||||
|
||||
class FileManager {
|
||||
constructor() {}
|
||||
constructor() {
|
||||
this.manifestDir = ".bmad-core";
|
||||
this.manifestFile = "install-manifest.yaml";
|
||||
}
|
||||
|
||||
async copyFile(source, destination) {
|
||||
try {
|
||||
@@ -16,9 +19,14 @@ class FileManager {
|
||||
|
||||
// Use streaming for large files (> 10MB)
|
||||
const stats = await fs.stat(source);
|
||||
await (stats.size > 10 * 1024 * 1024
|
||||
? pipeline(createReadStream(source), createWriteStream(destination))
|
||||
: fs.copy(source, destination));
|
||||
if (stats.size > 10 * 1024 * 1024) {
|
||||
await pipeline(
|
||||
createReadStream(source),
|
||||
createWriteStream(destination)
|
||||
);
|
||||
} else {
|
||||
await fs.copy(source, destination);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Failed to copy ${source}:`), error.message);
|
||||
@@ -33,20 +41,28 @@ class FileManager {
|
||||
// Use streaming copy for large directories
|
||||
const files = await resourceLocator.findFiles('**/*', {
|
||||
cwd: source,
|
||||
nodir: true,
|
||||
nodir: true
|
||||
});
|
||||
|
||||
// Process files in batches to avoid memory issues
|
||||
const batchSize = 50;
|
||||
for (let index = 0; index < files.length; index += batchSize) {
|
||||
const batch = files.slice(index, index + batchSize);
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
await Promise.all(
|
||||
batch.map((file) => this.copyFile(path.join(source, file), path.join(destination, file))),
|
||||
batch.map(file =>
|
||||
this.copyFile(
|
||||
path.join(source, file),
|
||||
path.join(destination, file)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Failed to copy directory ${source}:`), error.message);
|
||||
console.error(
|
||||
chalk.red(`Failed to copy directory ${source}:`),
|
||||
error.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -57,16 +73,17 @@ class FileManager {
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(sourceDir, file);
|
||||
const destinationPath = path.join(destDir, file);
|
||||
const destPath = path.join(destDir, file);
|
||||
|
||||
// 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;
|
||||
success = await (needsRootReplacement
|
||||
? this.copyFileWithRootReplacement(sourcePath, destinationPath, rootValue)
|
||||
: this.copyFile(sourcePath, destinationPath));
|
||||
if (needsRootReplacement) {
|
||||
success = await this.copyFileWithRootReplacement(sourcePath, destPath, rootValue);
|
||||
} else {
|
||||
success = await this.copyFile(sourcePath, destPath);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
copied.push(file);
|
||||
@@ -80,28 +97,32 @@ class FileManager {
|
||||
try {
|
||||
// Use streaming for hash calculation to reduce memory usage
|
||||
const stream = createReadStream(filePath);
|
||||
const hash = crypto.createHash('sha256');
|
||||
const hash = crypto.createHash("sha256");
|
||||
|
||||
for await (const chunk of stream) {
|
||||
hash.update(chunk);
|
||||
}
|
||||
|
||||
return hash.digest('hex').slice(0, 16);
|
||||
} catch {
|
||||
return hash.digest("hex").slice(0, 16);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async createManifest(installDir, config, files) {
|
||||
const manifestPath = path.join(installDir, this.manifestDir, this.manifestFile);
|
||||
const manifestPath = path.join(
|
||||
installDir,
|
||||
this.manifestDir,
|
||||
this.manifestFile
|
||||
);
|
||||
|
||||
// Read version from package.json
|
||||
let coreVersion = 'unknown';
|
||||
let coreVersion = "unknown";
|
||||
try {
|
||||
const packagePath = path.join(__dirname, '..', '..', '..', 'package.json');
|
||||
const packageJson = require(packagePath);
|
||||
coreVersion = packageJson.version;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.warn("Could not read version from package.json, using 'unknown'");
|
||||
}
|
||||
|
||||
@@ -135,23 +156,31 @@ class FileManager {
|
||||
}
|
||||
|
||||
async readManifest(installDir) {
|
||||
const manifestPath = path.join(installDir, this.manifestDir, this.manifestFile);
|
||||
const manifestPath = path.join(
|
||||
installDir,
|
||||
this.manifestDir,
|
||||
this.manifestFile
|
||||
);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(manifestPath, 'utf8');
|
||||
const content = await fs.readFile(manifestPath, "utf8");
|
||||
return yaml.load(content);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async readExpansionPackManifest(installDir, packId) {
|
||||
const manifestPath = path.join(installDir, `.${packId}`, this.manifestFile);
|
||||
const manifestPath = path.join(
|
||||
installDir,
|
||||
`.${packId}`,
|
||||
this.manifestFile
|
||||
);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(manifestPath, 'utf8');
|
||||
const content = await fs.readFile(manifestPath, "utf8");
|
||||
return yaml.load(content);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -174,7 +203,7 @@ class FileManager {
|
||||
async checkFileIntegrity(installDir, manifest) {
|
||||
const result = {
|
||||
missing: [],
|
||||
modified: [],
|
||||
modified: []
|
||||
};
|
||||
|
||||
for (const file of manifest.files) {
|
||||
@@ -185,13 +214,13 @@ class FileManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await this.pathExists(filePath)) {
|
||||
if (!(await this.pathExists(filePath))) {
|
||||
result.missing.push(file.path);
|
||||
} else {
|
||||
const currentHash = await this.calculateFileHash(filePath);
|
||||
if (currentHash && currentHash !== file.hash) {
|
||||
result.modified.push(file.path);
|
||||
}
|
||||
} else {
|
||||
result.missing.push(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +228,7 @@ class FileManager {
|
||||
}
|
||||
|
||||
async backupFile(filePath) {
|
||||
const backupPath = filePath + '.bak';
|
||||
const backupPath = filePath + ".bak";
|
||||
let counter = 1;
|
||||
let finalBackupPath = backupPath;
|
||||
|
||||
@@ -227,7 +256,7 @@ class FileManager {
|
||||
}
|
||||
|
||||
async readFile(filePath) {
|
||||
return fs.readFile(filePath, 'utf8');
|
||||
return fs.readFile(filePath, "utf8");
|
||||
}
|
||||
|
||||
async writeFile(filePath, content) {
|
||||
@@ -240,10 +269,14 @@ class FileManager {
|
||||
}
|
||||
|
||||
async createExpansionPackManifest(installDir, packId, config, files) {
|
||||
const manifestPath = path.join(installDir, `.${packId}`, this.manifestFile);
|
||||
const manifestPath = path.join(
|
||||
installDir,
|
||||
`.${packId}`,
|
||||
this.manifestFile
|
||||
);
|
||||
|
||||
const manifest = {
|
||||
version: config.expansionPackVersion || require('../../../package.json').version,
|
||||
version: config.expansionPackVersion || require("../../../package.json").version,
|
||||
installed_at: new Date().toISOString(),
|
||||
install_type: config.installType,
|
||||
expansion_pack_id: config.expansionPackId,
|
||||
@@ -303,27 +336,26 @@ class FileManager {
|
||||
// Check file size to determine if we should stream
|
||||
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
|
||||
const { Transform } = require('node:stream');
|
||||
const { Transform } = require('stream');
|
||||
const replaceStream = new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
const modified = chunk.toString().replaceAll('{root}', rootValue);
|
||||
const modified = chunk.toString().replace(/\{root\}/g, rootValue);
|
||||
callback(null, modified);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
await this.ensureDirectory(path.dirname(destination));
|
||||
await pipeline(
|
||||
createReadStream(source, { encoding: 'utf8' }),
|
||||
replaceStream,
|
||||
createWriteStream(destination, { encoding: 'utf8' }),
|
||||
createWriteStream(destination, { encoding: 'utf8' })
|
||||
);
|
||||
} else {
|
||||
// Regular approach for smaller files
|
||||
const content = await fsPromises.readFile(source, 'utf8');
|
||||
const updatedContent = content.replaceAll('{root}', rootValue);
|
||||
const updatedContent = content.replace(/\{root\}/g, rootValue);
|
||||
await this.ensureDirectory(path.dirname(destination));
|
||||
await fsPromises.writeFile(destination, updatedContent, 'utf8');
|
||||
}
|
||||
@@ -335,37 +367,32 @@ class FileManager {
|
||||
}
|
||||
}
|
||||
|
||||
async copyDirectoryWithRootReplacement(
|
||||
source,
|
||||
destination,
|
||||
rootValue,
|
||||
fileExtensions = ['.md', '.yaml', '.yml'],
|
||||
) {
|
||||
async copyDirectoryWithRootReplacement(source, destination, rootValue, fileExtensions = ['.md', '.yaml', '.yml']) {
|
||||
try {
|
||||
await this.ensureDirectory(destination);
|
||||
|
||||
// Get all files in source directory
|
||||
const files = await resourceLocator.findFiles('**/*', {
|
||||
cwd: source,
|
||||
nodir: true,
|
||||
nodir: true
|
||||
});
|
||||
|
||||
let replacedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(source, file);
|
||||
const destinationPath = path.join(destination, file);
|
||||
const destPath = path.join(destination, file);
|
||||
|
||||
// Check if this file type should have {root} replacement
|
||||
const shouldReplace = fileExtensions.some((extension) => file.endsWith(extension));
|
||||
const shouldReplace = fileExtensions.some(ext => file.endsWith(ext));
|
||||
|
||||
if (shouldReplace) {
|
||||
if (await this.copyFileWithRootReplacement(sourcePath, destinationPath, rootValue)) {
|
||||
if (await this.copyFileWithRootReplacement(sourcePath, destPath, rootValue)) {
|
||||
replacedCount++;
|
||||
}
|
||||
} else {
|
||||
// Regular copy for files that don't need replacement
|
||||
await this.copyFile(sourcePath, destinationPath);
|
||||
await this.copyFile(sourcePath, destPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,15 +402,10 @@ class FileManager {
|
||||
|
||||
return true;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
manifestDir = '.bmad-core';
|
||||
manifestFile = 'install-manifest.yaml';
|
||||
}
|
||||
|
||||
module.exports = new FileManager();
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
* Reduces duplication and provides shared methods
|
||||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('js-yaml');
|
||||
const chalk = require('chalk').default || require('chalk');
|
||||
const fileManager = require('./file-manager');
|
||||
const resourceLocator = require('./resource-locator');
|
||||
const { extractYamlFromAgent } = require('../../lib/yaml-utils');
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const yaml = require("js-yaml");
|
||||
const chalk = require("chalk").default || require("chalk");
|
||||
const fileManager = require("./file-manager");
|
||||
const resourceLocator = require("./resource-locator");
|
||||
const { extractYamlFromAgent } = require("../../lib/yaml-utils");
|
||||
|
||||
class BaseIdeSetup {
|
||||
constructor() {
|
||||
@@ -30,16 +30,16 @@ class BaseIdeSetup {
|
||||
|
||||
// Get core agents
|
||||
const coreAgents = await this.getCoreAgentIds(installDir);
|
||||
for (const id of coreAgents) allAgents.add(id);
|
||||
coreAgents.forEach(id => allAgents.add(id));
|
||||
|
||||
// Get expansion pack agents
|
||||
const expansionPacks = await this.getInstalledExpansionPacks(installDir);
|
||||
for (const pack of expansionPacks) {
|
||||
const packAgents = await this.getExpansionPackAgents(pack.path);
|
||||
for (const id of packAgents) allAgents.add(id);
|
||||
packAgents.forEach(id => allAgents.add(id));
|
||||
}
|
||||
|
||||
const result = [...allAgents];
|
||||
const result = Array.from(allAgents);
|
||||
this._agentCache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
@@ -50,14 +50,14 @@ class BaseIdeSetup {
|
||||
async getCoreAgentIds(installDir) {
|
||||
const coreAgents = [];
|
||||
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) {
|
||||
if (await fileManager.pathExists(agentsDir)) {
|
||||
const files = await resourceLocator.findFiles('*.md', { cwd: agentsDir });
|
||||
coreAgents.push(...files.map((file) => path.basename(file, '.md')));
|
||||
const files = await resourceLocator.findFiles("*.md", { cwd: agentsDir });
|
||||
coreAgents.push(...files.map(file => path.basename(file, ".md")));
|
||||
break; // Use first found
|
||||
}
|
||||
}
|
||||
@@ -80,9 +80,9 @@ class BaseIdeSetup {
|
||||
if (!agentPath) {
|
||||
// Check installation-specific paths
|
||||
const possiblePaths = [
|
||||
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, ".bmad-core", "agents", `${agentId}.md`),
|
||||
path.join(installDir, "bmad-core", "agents", `${agentId}.md`),
|
||||
path.join(installDir, "common", "agents", `${agentId}.md`)
|
||||
];
|
||||
|
||||
for (const testPath of possiblePaths) {
|
||||
@@ -113,7 +113,7 @@ class BaseIdeSetup {
|
||||
const metadata = yaml.load(yamlContent);
|
||||
return metadata.agent_name || agentId;
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Fallback to agent ID
|
||||
}
|
||||
return agentId;
|
||||
@@ -131,29 +131,29 @@ class BaseIdeSetup {
|
||||
const expansionPacks = [];
|
||||
|
||||
// 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) {
|
||||
if (dotExpansion !== '.bmad-core') {
|
||||
if (dotExpansion !== ".bmad-core") {
|
||||
const packPath = path.join(installDir, dotExpansion);
|
||||
const packName = dotExpansion.slice(1); // remove the dot
|
||||
const packName = dotExpansion.substring(1); // remove the dot
|
||||
expansionPacks.push({
|
||||
name: packName,
|
||||
path: packPath,
|
||||
path: packPath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (!folder.startsWith('.bmad-') && folder !== '.bmad-core') {
|
||||
if (!folder.startsWith(".bmad-") && folder !== ".bmad-core") {
|
||||
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)) {
|
||||
expansionPacks.push({
|
||||
name: folder.slice(1), // remove the dot
|
||||
path: packPath,
|
||||
name: folder.substring(1), // remove the dot
|
||||
path: packPath
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -167,13 +167,13 @@ class BaseIdeSetup {
|
||||
* Get expansion pack agents
|
||||
*/
|
||||
async getExpansionPackAgents(packPath) {
|
||||
const agentsDir = path.join(packPath, 'agents');
|
||||
const agentsDir = path.join(packPath, "agents");
|
||||
if (!(await fileManager.pathExists(agentsDir))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const agentFiles = await resourceLocator.findFiles('*.md', { cwd: agentsDir });
|
||||
return agentFiles.map((file) => path.basename(file, '.md'));
|
||||
const agentFiles = await resourceLocator.findFiles("*.md", { cwd: agentsDir });
|
||||
return agentFiles.map(file => path.basename(file, ".md"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,27 +184,26 @@ class BaseIdeSetup {
|
||||
const agentTitle = await this.getAgentTitle(agentId, installDir);
|
||||
const yamlContent = extractYamlFromAgent(agentContent);
|
||||
|
||||
let content = '';
|
||||
let content = "";
|
||||
|
||||
if (format === 'mdc') {
|
||||
// MDC format for Cursor
|
||||
content = '---\n';
|
||||
content += 'description: \n';
|
||||
content += 'globs: []\n';
|
||||
content += 'alwaysApply: false\n';
|
||||
content += '---\n\n';
|
||||
content = "---\n";
|
||||
content += "description: \n";
|
||||
content += "globs: []\n";
|
||||
content += "alwaysApply: false\n";
|
||||
content += "---\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 += '## 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 += '```yaml\n';
|
||||
content += yamlContent || agentContent.replace(/^#.*$/m, '').trim();
|
||||
content += '\n```\n\n';
|
||||
content += '## File Reference\n\n';
|
||||
const relativePath = path.relative(installDir, agentPath).replaceAll('\\', '/');
|
||||
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 += "```yaml\n";
|
||||
content += yamlContent || agentContent.replace(/^#.*$/m, "").trim();
|
||||
content += "\n```\n\n";
|
||||
content += "## File Reference\n\n";
|
||||
const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
|
||||
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`;
|
||||
} else if (format === 'claude') {
|
||||
// Claude Code format
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
* Helps identify memory leaks and optimize resource usage
|
||||
*/
|
||||
|
||||
const v8 = require('node:v8');
|
||||
const v8 = require('v8');
|
||||
|
||||
class MemoryProfiler {
|
||||
constructor() {
|
||||
@@ -28,18 +28,18 @@ class MemoryProfiler {
|
||||
heapTotal: this.formatBytes(memUsage.heapTotal),
|
||||
heapUsed: this.formatBytes(memUsage.heapUsed),
|
||||
external: this.formatBytes(memUsage.external),
|
||||
arrayBuffers: this.formatBytes(memUsage.arrayBuffers || 0),
|
||||
arrayBuffers: this.formatBytes(memUsage.arrayBuffers || 0)
|
||||
},
|
||||
heap: {
|
||||
totalHeapSize: this.formatBytes(heapStats.total_heap_size),
|
||||
usedHeapSize: this.formatBytes(heapStats.used_heap_size),
|
||||
heapSizeLimit: this.formatBytes(heapStats.heap_size_limit),
|
||||
mallocedMemory: this.formatBytes(heapStats.malloced_memory),
|
||||
externalMemory: this.formatBytes(heapStats.external_memory),
|
||||
externalMemory: this.formatBytes(heapStats.external_memory)
|
||||
},
|
||||
raw: {
|
||||
heapUsed: memUsage.heapUsed,
|
||||
},
|
||||
heapUsed: memUsage.heapUsed
|
||||
}
|
||||
};
|
||||
|
||||
// Track peak memory
|
||||
@@ -55,8 +55,8 @@ class MemoryProfiler {
|
||||
* Force garbage collection (requires --expose-gc flag)
|
||||
*/
|
||||
forceGC() {
|
||||
if (globalThis.gc) {
|
||||
globalThis.gc();
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -72,11 +72,11 @@ class MemoryProfiler {
|
||||
currentUsage: {
|
||||
rss: this.formatBytes(currentMemory.rss),
|
||||
heapTotal: this.formatBytes(currentMemory.heapTotal),
|
||||
heapUsed: this.formatBytes(currentMemory.heapUsed),
|
||||
heapUsed: this.formatBytes(currentMemory.heapUsed)
|
||||
},
|
||||
peakMemory: this.formatBytes(this.peakMemory),
|
||||
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,
|
||||
memoryGrowth,
|
||||
checkpoints: this.checkpoints,
|
||||
recommendations: this.getRecommendations(memoryGrowth),
|
||||
recommendations: this.getRecommendations(memoryGrowth)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -102,18 +102,18 @@ class MemoryProfiler {
|
||||
if (this.checkpoints.length < 2) return [];
|
||||
|
||||
const growth = [];
|
||||
for (let index = 1; index < this.checkpoints.length; index++) {
|
||||
const previous = this.checkpoints[index - 1];
|
||||
const current = this.checkpoints[index];
|
||||
for (let i = 1; i < this.checkpoints.length; i++) {
|
||||
const prev = this.checkpoints[i - 1];
|
||||
const curr = this.checkpoints[i];
|
||||
|
||||
const heapDiff = current.raw.heapUsed - previous.raw.heapUsed;
|
||||
const heapDiff = curr.raw.heapUsed - prev.raw.heapUsed;
|
||||
|
||||
growth.push({
|
||||
from: previous.label,
|
||||
to: current.label,
|
||||
from: prev.label,
|
||||
to: curr.label,
|
||||
heapGrowth: this.formatBytes(Math.abs(heapDiff)),
|
||||
isIncrease: heapDiff > 0,
|
||||
timeDiff: `${((current.timestamp - previous.timestamp) / 1000).toFixed(2)}s`,
|
||||
timeDiff: `${((curr.timestamp - prev.timestamp) / 1000).toFixed(2)}s`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ class MemoryProfiler {
|
||||
const recommendations = [];
|
||||
|
||||
// Check for large memory growth
|
||||
const largeGrowths = memoryGrowth.filter((g) => {
|
||||
const largeGrowths = memoryGrowth.filter(g => {
|
||||
const bytes = this.parseBytes(g.heapGrowth);
|
||||
return bytes > 50 * 1024 * 1024; // 50MB
|
||||
});
|
||||
@@ -136,17 +136,16 @@ class MemoryProfiler {
|
||||
recommendations.push({
|
||||
type: 'warning',
|
||||
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
|
||||
if (this.peakMemory > 500 * 1024 * 1024) {
|
||||
// 500MB
|
||||
if (this.peakMemory > 500 * 1024 * 1024) { // 500MB
|
||||
recommendations.push({
|
||||
type: 'warning',
|
||||
message: `High peak memory usage: ${this.formatBytes(this.peakMemory)}`,
|
||||
suggestion: 'Consider processing files in smaller batches',
|
||||
suggestion: 'Consider processing files in smaller batches'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,7 +155,7 @@ class MemoryProfiler {
|
||||
recommendations.push({
|
||||
type: 'error',
|
||||
message: 'Potential memory leak detected',
|
||||
details: 'Memory usage continuously increases without significant decreases',
|
||||
details: 'Memory usage continuously increases without significant decreases'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -170,8 +169,8 @@ class MemoryProfiler {
|
||||
if (this.checkpoints.length < 5) return false;
|
||||
|
||||
let increasingCount = 0;
|
||||
for (let index = 1; index < this.checkpoints.length; index++) {
|
||||
if (this.checkpoints[index].raw.heapUsed > this.checkpoints[index - 1].raw.heapUsed) {
|
||||
for (let i = 1; i < this.checkpoints.length; i++) {
|
||||
if (this.checkpoints[i].raw.heapUsed > this.checkpoints[i - 1].raw.heapUsed) {
|
||||
increasingCount++;
|
||||
}
|
||||
}
|
||||
@@ -188,26 +187,26 @@ class MemoryProfiler {
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const index = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return Number.parseFloat((bytes / Math.pow(k, index)).toFixed(2)) + ' ' + sizes[index];
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse human-readable bytes back to number
|
||||
*/
|
||||
parseBytes(string_) {
|
||||
const match = string_.match(/^([\d.]+)\s*([KMGT]?B?)$/i);
|
||||
parseBytes(str) {
|
||||
const match = str.match(/^([\d.]+)\s*([KMGT]?B?)$/i);
|
||||
if (!match) return 0;
|
||||
|
||||
const value = Number.parseFloat(match[1]);
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
|
||||
const multipliers = {
|
||||
B: 1,
|
||||
KB: 1024,
|
||||
MB: 1024 * 1024,
|
||||
GB: 1024 * 1024 * 1024,
|
||||
'B': 1,
|
||||
'KB': 1024,
|
||||
'MB': 1024 * 1024,
|
||||
'GB': 1024 * 1024 * 1024
|
||||
};
|
||||
|
||||
return value * (multipliers[unit] || 1);
|
||||
|
||||
@@ -17,13 +17,13 @@ class ModuleManager {
|
||||
const modules = await Promise.all([
|
||||
this.getModule('chalk'),
|
||||
this.getModule('ora'),
|
||||
this.getModule('inquirer'),
|
||||
this.getModule('inquirer')
|
||||
]);
|
||||
|
||||
return {
|
||||
chalk: modules[0],
|
||||
ora: modules[1],
|
||||
inquirer: modules[2],
|
||||
inquirer: modules[2]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,24 +64,18 @@ class ModuleManager {
|
||||
*/
|
||||
async _loadModule(moduleName) {
|
||||
switch (moduleName) {
|
||||
case 'chalk': {
|
||||
case 'chalk':
|
||||
return (await import('chalk')).default;
|
||||
}
|
||||
case 'ora': {
|
||||
case 'ora':
|
||||
return (await import('ora')).default;
|
||||
}
|
||||
case 'inquirer': {
|
||||
case 'inquirer':
|
||||
return (await import('inquirer')).default;
|
||||
}
|
||||
case 'glob': {
|
||||
case 'glob':
|
||||
return (await import('glob')).glob;
|
||||
}
|
||||
case 'globSync': {
|
||||
case 'globSync':
|
||||
return (await import('glob')).globSync;
|
||||
}
|
||||
default: {
|
||||
default:
|
||||
throw new Error(`Unknown module: ${moduleName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,11 +93,13 @@ class ModuleManager {
|
||||
* @returns {Promise<Object>} Object with module names as keys
|
||||
*/
|
||||
async getModules(moduleNames) {
|
||||
const modules = await Promise.all(moduleNames.map((name) => this.getModule(name)));
|
||||
const modules = await Promise.all(
|
||||
moduleNames.map(name => this.getModule(name))
|
||||
);
|
||||
|
||||
return moduleNames.reduce((accumulator, name, index) => {
|
||||
accumulator[name] = modules[index];
|
||||
return accumulator;
|
||||
return moduleNames.reduce((acc, name, index) => {
|
||||
acc[name] = modules[index];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,11 +107,14 @@ class ResourceLocator {
|
||||
|
||||
// Get agents from bmad-core
|
||||
const coreAgents = await this.findFiles('agents/*.md', {
|
||||
cwd: this.getBmadCorePath(),
|
||||
cwd: this.getBmadCorePath()
|
||||
});
|
||||
|
||||
for (const agentFile of coreAgents) {
|
||||
const content = await fs.readFile(path.join(this.getBmadCorePath(), agentFile), 'utf8');
|
||||
const content = await fs.readFile(
|
||||
path.join(this.getBmadCorePath(), agentFile),
|
||||
'utf8'
|
||||
);
|
||||
const yamlContent = extractYamlFromAgent(content);
|
||||
if (yamlContent) {
|
||||
try {
|
||||
@@ -120,9 +123,9 @@ class ResourceLocator {
|
||||
id: path.basename(agentFile, '.md'),
|
||||
name: metadata.agent_name || path.basename(agentFile, '.md'),
|
||||
description: metadata.description || 'No description available',
|
||||
source: 'core',
|
||||
source: 'core'
|
||||
});
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// Skip invalid agents
|
||||
}
|
||||
}
|
||||
@@ -164,12 +167,11 @@ class ResourceLocator {
|
||||
name: config.name || entry.name,
|
||||
version: config.version || '1.0.0',
|
||||
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',
|
||||
path: path.join(expansionPacksPath, entry.name),
|
||||
path: path.join(expansionPacksPath, entry.name)
|
||||
});
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// Skip invalid packs
|
||||
}
|
||||
}
|
||||
@@ -205,7 +207,7 @@ class ResourceLocator {
|
||||
const config = yaml.load(content);
|
||||
this._pathCache.set(cacheKey, config);
|
||||
return config;
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -259,7 +261,7 @@ class ResourceLocator {
|
||||
const result = { all: allDeps, byType };
|
||||
this._pathCache.set(cacheKey, result);
|
||||
return result;
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return { all: [], byType: {} };
|
||||
}
|
||||
}
|
||||
@@ -293,7 +295,7 @@ class ResourceLocator {
|
||||
const config = yaml.load(content);
|
||||
this._pathCache.set(cacheKey, config);
|
||||
return config;
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
{
|
||||
"name": "bmad-method",
|
||||
"version": "5.1.3",
|
||||
"version": "4.37.0-beta.6",
|
||||
"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": [
|
||||
"bmad",
|
||||
"agile",
|
||||
@@ -11,24 +19,8 @@
|
||||
"installer",
|
||||
"agents"
|
||||
],
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^14.0.0",
|
||||
@@ -40,5 +32,13 @@
|
||||
},
|
||||
"engines": {
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const fs = require('node:fs').promises;
|
||||
const path = require('node:path');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const { extractYamlFromAgent } = require('./yaml-utils');
|
||||
|
||||
@@ -28,9 +28,9 @@ class DependencyResolver {
|
||||
id: agentId,
|
||||
path: agentPath,
|
||||
content: agentContent,
|
||||
config: agentConfig,
|
||||
config: agentConfig
|
||||
},
|
||||
resources: [],
|
||||
resources: []
|
||||
};
|
||||
|
||||
// Personas are now embedded in agent configs, no need to resolve separately
|
||||
@@ -58,18 +58,18 @@ class DependencyResolver {
|
||||
id: teamId,
|
||||
path: teamPath,
|
||||
content: teamContent,
|
||||
config: teamConfig,
|
||||
config: teamConfig
|
||||
},
|
||||
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
|
||||
const bmadAgent = await this.resolveAgentDependencies('bmad-orchestrator');
|
||||
dependencies.agents.push(bmadAgent.agent);
|
||||
for (const res of bmadAgent.resources) {
|
||||
bmadAgent.resources.forEach(res => {
|
||||
dependencies.resources.set(res.path, res);
|
||||
}
|
||||
});
|
||||
|
||||
// Resolve all agents in the team
|
||||
let agentsToResolve = teamConfig.agents || [];
|
||||
@@ -78,7 +78,7 @@ class DependencyResolver {
|
||||
if (agentsToResolve.includes('*')) {
|
||||
const allAgents = await this.listAgents();
|
||||
// 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) {
|
||||
if (!agentsToResolve.includes(agent) && agent !== 'bmad-master') {
|
||||
agentsToResolve.push(agent);
|
||||
@@ -92,9 +92,9 @@ class DependencyResolver {
|
||||
dependencies.agents.push(agentDeps.agent);
|
||||
|
||||
// Add resources with deduplication
|
||||
for (const res of agentDeps.resources) {
|
||||
agentDeps.resources.forEach(res => {
|
||||
dependencies.resources.set(res.path, res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve workflows
|
||||
@@ -104,7 +104,7 @@ class DependencyResolver {
|
||||
}
|
||||
|
||||
// Convert Map back to array
|
||||
dependencies.resources = [...dependencies.resources.values()];
|
||||
dependencies.resources = Array.from(dependencies.resources.values());
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
@@ -123,12 +123,12 @@ class DependencyResolver {
|
||||
try {
|
||||
filePath = path.join(this.bmadCore, type, id);
|
||||
content = await fs.readFile(filePath, 'utf8');
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// If not found in bmad-core, try common folder
|
||||
try {
|
||||
filePath = path.join(this.common, type, id);
|
||||
content = await fs.readFile(filePath, 'utf8');
|
||||
} catch {
|
||||
} catch (e2) {
|
||||
// File not found in either location
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ class DependencyResolver {
|
||||
type,
|
||||
id,
|
||||
path: filePath,
|
||||
content,
|
||||
content
|
||||
};
|
||||
|
||||
this.cache.set(cacheKey, resource);
|
||||
@@ -156,8 +156,10 @@ class DependencyResolver {
|
||||
async listAgents() {
|
||||
try {
|
||||
const files = await fs.readdir(path.join(this.bmadCore, 'agents'));
|
||||
return files.filter((f) => f.endsWith('.md')).map((f) => f.replace('.md', ''));
|
||||
} catch {
|
||||
return files
|
||||
.filter(f => f.endsWith('.md'))
|
||||
.map(f => f.replace('.md', ''));
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -165,8 +167,10 @@ class DependencyResolver {
|
||||
async listTeams() {
|
||||
try {
|
||||
const files = await fs.readdir(path.join(this.bmadCore, 'agent-teams'));
|
||||
return files.filter((f) => f.endsWith('.yaml')).map((f) => f.replace('.yaml', ''));
|
||||
} catch {
|
||||
return files
|
||||
.filter(f => f.endsWith('.yaml'))
|
||||
.map(f => f.replace('.yaml', ''));
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
function extractYamlFromAgent(agentContent, cleanCommands = false) {
|
||||
// Remove carriage returns and match YAML block
|
||||
const yamlMatch = agentContent.replaceAll('\r', '').match(/```ya?ml\n([\s\S]*?)\n```/);
|
||||
const yamlMatch = agentContent.replace(/\r/g, "").match(/```ya?ml\n([\s\S]*?)\n```/);
|
||||
if (!yamlMatch) return null;
|
||||
|
||||
let yamlContent = yamlMatch[1].trim();
|
||||
@@ -18,12 +18,12 @@ function extractYamlFromAgent(agentContent, cleanCommands = false) {
|
||||
// Clean up command descriptions if requested
|
||||
// Converts "- command - description" to just "- command"
|
||||
if (cleanCommands) {
|
||||
yamlContent = yamlContent.replaceAll(/^(\s*-)(\s*"[^"]+")(\s*-\s*.*)$/gm, '$1$2');
|
||||
yamlContent = yamlContent.replace(/^(\s*-)(\s*"[^"]+")(\s*-\s*.*)$/gm, '$1$2');
|
||||
}
|
||||
|
||||
return yamlContent;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractYamlFromAgent,
|
||||
extractYamlFromAgent
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
const { execSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
|
||||
// Get the latest stable tag (exclude beta tags)
|
||||
const allTags = execSync('git tag -l | sort -V', { encoding: 'utf8' }).split('\n').filter(Boolean);
|
||||
const stableTags = allTags.filter((tag) => !tag.includes('beta'));
|
||||
const latestTag = stableTags.at(-1) || 'v5.0.0';
|
||||
|
||||
// Get commits since last tag
|
||||
const commits = execSync(`git log ${latestTag}..HEAD --pretty=format:"- %s" --reverse`, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
|
||||
// Categorize commits
|
||||
const features = commits.filter((commit) => /^- (feat|Feature)/.test(commit));
|
||||
const fixes = commits.filter((commit) => /^- (fix|Fix)/.test(commit));
|
||||
const chores = commits.filter((commit) => /^- (chore|Chore)/.test(commit));
|
||||
const others = commits.filter(
|
||||
(commit) => !/^- (feat|Feature|fix|Fix|chore|Chore|release:|Release:)/.test(commit),
|
||||
);
|
||||
|
||||
// Get next version (you can modify this logic)
|
||||
const currentVersion = require('../package.json').version;
|
||||
const versionParts = currentVersion.split('.').map(Number);
|
||||
const nextVersion = `${versionParts[0]}.${versionParts[1] + 1}.0`; // Default to minor bump
|
||||
|
||||
console.log(`## 🚀 What's New in v${nextVersion}\n`);
|
||||
|
||||
if (features.length > 0) {
|
||||
console.log('### ✨ New Features');
|
||||
for (const feature of features) console.log(feature);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (fixes.length > 0) {
|
||||
console.log('### 🐛 Bug Fixes');
|
||||
for (const fix of fixes) console.log(fix);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (others.length > 0) {
|
||||
console.log('### 📦 Other Changes');
|
||||
for (const other of others) console.log(other);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (chores.length > 0) {
|
||||
console.log('### 🔧 Maintenance');
|
||||
for (const chore of chores) console.log(chore);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('\n## 📦 Installation\n');
|
||||
console.log('```bash');
|
||||
console.log('npx bmad-method install');
|
||||
console.log('```');
|
||||
|
||||
console.log(
|
||||
`\n**Full Changelog**: https://github.com/bmadcode/BMAD-METHOD/compare/${latestTag}...v${nextVersion}`,
|
||||
);
|
||||
|
||||
console.log(`\n---\n📊 **Summary**: ${commits.length} commits since ${latestTag}`);
|
||||
console.log(`🏷️ **Previous tag**: ${latestTag}`);
|
||||
console.log(`🚀 **Next version**: v${nextVersion} (estimated)`);
|
||||
30
tools/semantic-release-sync-installer.js
Normal file
30
tools/semantic-release-sync-installer.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Semantic-release plugin to sync installer package.json version
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// This function runs during the "prepare" step of semantic-release
|
||||
function prepare(_, { nextRelease, logger }) {
|
||||
// Define the path to the installer package.json file
|
||||
const file = path.join(process.cwd(), 'tools/installer/package.json');
|
||||
|
||||
// If the file does not exist, skip syncing and log a message
|
||||
if (!fs.existsSync(file)) return logger.log('Installer package.json not found, skipping');
|
||||
|
||||
// Read and parse the package.json file
|
||||
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
|
||||
// Update the version field with the next release version
|
||||
pkg.version = nextRelease.version;
|
||||
|
||||
// Write the updated JSON back to the file
|
||||
fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');
|
||||
|
||||
// Log success message
|
||||
logger.log(`Synced installer package.json to version ${nextRelease.version}`);
|
||||
}
|
||||
|
||||
// Export the prepare function so semantic-release can use it
|
||||
module.exports = { prepare };
|
||||
@@ -1,8 +1,8 @@
|
||||
// ASCII banner art definitions extracted from banners.js to separate art from logic
|
||||
|
||||
const BMAD_TITLE = 'BMAD-METHOD';
|
||||
const FLATTENER_TITLE = 'FLATTENER';
|
||||
const INSTALLER_TITLE = 'INSTALLER';
|
||||
const BMAD_TITLE = "BMAD-METHOD";
|
||||
const FLATTENER_TITLE = "FLATTENER";
|
||||
const INSTALLER_TITLE = "INSTALLER";
|
||||
|
||||
// Large ASCII blocks (block-style fonts)
|
||||
const BMAD_LARGE = `
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Sync installer package.json version with main package.json
|
||||
* Used by semantic-release to keep versions in sync
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function syncInstallerVersion() {
|
||||
// Read main package.json
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const arguments_ = process.argv.slice(2);
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (arguments_.length < 2) {
|
||||
if (args.length < 2) {
|
||||
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');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [packId, newVersion] = arguments_;
|
||||
const [packId, newVersion] = args;
|
||||
|
||||
// Validate version format
|
||||
if (!/^\d+\.\d+\.\d+$/.test(newVersion)) {
|
||||
@@ -41,9 +43,8 @@ async function updateVersion() {
|
||||
console.log(`\n✓ Successfully updated ${packId} to version ${newVersion}`);
|
||||
console.log('\nNext steps:');
|
||||
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) {
|
||||
console.error('Error updating version:', error.message);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
const fs = require('node:fs').promises;
|
||||
const path = require('node:path');
|
||||
const { glob } = require('glob');
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
const { glob } = require("glob");
|
||||
|
||||
// Dynamic imports for ES modules
|
||||
let chalk, ora, inquirer;
|
||||
|
||||
// Initialize ES modules
|
||||
async function initializeModules() {
|
||||
chalk = (await import('chalk')).default;
|
||||
ora = (await import('ora')).default;
|
||||
inquirer = (await import('inquirer')).default;
|
||||
chalk = (await import("chalk")).default;
|
||||
ora = (await import("ora")).default;
|
||||
inquirer = (await import("inquirer")).default;
|
||||
}
|
||||
|
||||
class V3ToV4Upgrader {
|
||||
@@ -25,15 +25,23 @@ class V3ToV4Upgrader {
|
||||
process.stdin.resume();
|
||||
|
||||
// 1. Welcome message
|
||||
console.log(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('- Creates a backup of your V3 files (.bmad-v3-backup/)');
|
||||
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.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');
|
||||
console.log(
|
||||
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("- Creates a backup of your V3 files (.bmad-v3-backup/)");
|
||||
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.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
|
||||
const projectPath = await this.getProjectPath(options.projectPath);
|
||||
@@ -41,11 +49,15 @@ class V3ToV4Upgrader {
|
||||
// 3. Validate V3 structure
|
||||
const validation = await this.validateV3Project(projectPath);
|
||||
if (!validation.isValid) {
|
||||
console.error(chalk.red("\nError: This doesn't appear to be a V3 project."));
|
||||
console.error('Expected to find:');
|
||||
console.error('- bmad-agent/ directory');
|
||||
console.error('- docs/ directory\n');
|
||||
console.error("Please check you're in the correct directory and try again.");
|
||||
console.error(
|
||||
chalk.red("\nError: This doesn't appear to be a V3 project.")
|
||||
);
|
||||
console.error("Expected to find:");
|
||||
console.error("- bmad-agent/ directory");
|
||||
console.error("- docs/ directory\n");
|
||||
console.error(
|
||||
"Please check you're in the correct directory and try again."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,15 +68,15 @@ class V3ToV4Upgrader {
|
||||
if (!options.dryRun) {
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Continue with upgrade?',
|
||||
type: "confirm",
|
||||
name: "confirm",
|
||||
message: "Continue with upgrade?",
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!confirm) {
|
||||
console.log('Upgrade cancelled.');
|
||||
console.log("Upgrade cancelled.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -94,7 +106,7 @@ class V3ToV4Upgrader {
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(chalk.red('\nUpgrade error:'), error.message);
|
||||
console.error(chalk.red("\nUpgrade error:"), error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -106,9 +118,9 @@ class V3ToV4Upgrader {
|
||||
|
||||
const { projectPath } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'projectPath',
|
||||
message: 'Please enter the path to your V3 project:',
|
||||
type: "input",
|
||||
name: "projectPath",
|
||||
message: "Please enter the path to your V3 project:",
|
||||
default: process.cwd(),
|
||||
},
|
||||
]);
|
||||
@@ -117,45 +129,45 @@ class V3ToV4Upgrader {
|
||||
}
|
||||
|
||||
async validateV3Project(projectPath) {
|
||||
const spinner = ora('Validating project structure...').start();
|
||||
const spinner = ora("Validating project structure...").start();
|
||||
|
||||
try {
|
||||
const bmadAgentPath = path.join(projectPath, 'bmad-agent');
|
||||
const docsPath = path.join(projectPath, 'docs');
|
||||
const bmadAgentPath = path.join(projectPath, "bmad-agent");
|
||||
const docsPath = path.join(projectPath, "docs");
|
||||
|
||||
const hasBmadAgent = await this.pathExists(bmadAgentPath);
|
||||
const hasDocs = await this.pathExists(docsPath);
|
||||
|
||||
if (hasBmadAgent) {
|
||||
spinner.text = '✓ Found bmad-agent/ directory';
|
||||
console.log(chalk.green('\n✓ Found bmad-agent/ directory'));
|
||||
spinner.text = "✓ Found bmad-agent/ directory";
|
||||
console.log(chalk.green("\n✓ Found bmad-agent/ directory"));
|
||||
}
|
||||
|
||||
if (hasDocs) {
|
||||
console.log(chalk.green('✓ Found docs/ directory'));
|
||||
console.log(chalk.green("✓ Found docs/ directory"));
|
||||
}
|
||||
|
||||
const isValid = hasBmadAgent && hasDocs;
|
||||
|
||||
if (isValid) {
|
||||
spinner.succeed('This appears to be a valid V3 project');
|
||||
spinner.succeed("This appears to be a valid V3 project");
|
||||
} else {
|
||||
spinner.fail('Invalid V3 project structure');
|
||||
spinner.fail("Invalid V3 project structure");
|
||||
}
|
||||
|
||||
return { isValid, hasBmadAgent, hasDocs };
|
||||
} catch (error) {
|
||||
spinner.fail('Validation failed');
|
||||
spinner.fail("Validation failed");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async analyzeProject(projectPath) {
|
||||
const docsPath = path.join(projectPath, 'docs');
|
||||
const bmadAgentPath = path.join(projectPath, 'bmad-agent');
|
||||
const docsPath = path.join(projectPath, "docs");
|
||||
const bmadAgentPath = path.join(projectPath, "bmad-agent");
|
||||
|
||||
// Find PRD
|
||||
const prdCandidates = ['prd.md', 'PRD.md', 'product-requirements.md'];
|
||||
const prdCandidates = ["prd.md", "PRD.md", "product-requirements.md"];
|
||||
let prdFile = null;
|
||||
for (const candidate of prdCandidates) {
|
||||
const candidatePath = path.join(docsPath, candidate);
|
||||
@@ -166,7 +178,11 @@ class V3ToV4Upgrader {
|
||||
}
|
||||
|
||||
// Find Architecture
|
||||
const archCandidates = ['architecture.md', 'Architecture.md', 'technical-architecture.md'];
|
||||
const archCandidates = [
|
||||
"architecture.md",
|
||||
"Architecture.md",
|
||||
"technical-architecture.md",
|
||||
];
|
||||
let archFile = null;
|
||||
for (const candidate of archCandidates) {
|
||||
const candidatePath = path.join(docsPath, candidate);
|
||||
@@ -178,9 +194,9 @@ class V3ToV4Upgrader {
|
||||
|
||||
// Find Front-end Architecture (V3 specific)
|
||||
const frontEndCandidates = [
|
||||
'front-end-architecture.md',
|
||||
'frontend-architecture.md',
|
||||
'ui-architecture.md',
|
||||
"front-end-architecture.md",
|
||||
"frontend-architecture.md",
|
||||
"ui-architecture.md",
|
||||
];
|
||||
let frontEndArchFile = null;
|
||||
for (const candidate of frontEndCandidates) {
|
||||
@@ -193,10 +209,10 @@ class V3ToV4Upgrader {
|
||||
|
||||
// Find UX/UI spec
|
||||
const uxSpecCandidates = [
|
||||
'ux-ui-spec.md',
|
||||
'ux-ui-specification.md',
|
||||
'ui-spec.md',
|
||||
'ux-spec.md',
|
||||
"ux-ui-spec.md",
|
||||
"ux-ui-specification.md",
|
||||
"ui-spec.md",
|
||||
"ux-spec.md",
|
||||
];
|
||||
let uxSpecFile = null;
|
||||
for (const candidate of uxSpecCandidates) {
|
||||
@@ -208,7 +224,12 @@ class V3ToV4Upgrader {
|
||||
}
|
||||
|
||||
// Find v0 prompt or UX prompt
|
||||
const uxPromptCandidates = ['v0-prompt.md', 'ux-prompt.md', 'ui-prompt.md', 'design-prompt.md'];
|
||||
const uxPromptCandidates = [
|
||||
"v0-prompt.md",
|
||||
"ux-prompt.md",
|
||||
"ui-prompt.md",
|
||||
"design-prompt.md",
|
||||
];
|
||||
let uxPromptFile = null;
|
||||
for (const candidate of uxPromptCandidates) {
|
||||
const candidatePath = path.join(docsPath, candidate);
|
||||
@@ -219,19 +240,19 @@ class V3ToV4Upgrader {
|
||||
}
|
||||
|
||||
// Find epic files
|
||||
const epicFiles = await glob('epic*.md', { cwd: docsPath });
|
||||
const epicFiles = await glob("epic*.md", { cwd: docsPath });
|
||||
|
||||
// Find story files
|
||||
const storiesPath = path.join(docsPath, 'stories');
|
||||
const storiesPath = path.join(docsPath, "stories");
|
||||
let storyFiles = [];
|
||||
if (await this.pathExists(storiesPath)) {
|
||||
storyFiles = await glob('*.md', { cwd: storiesPath });
|
||||
storyFiles = await glob("*.md", { cwd: storiesPath });
|
||||
}
|
||||
|
||||
// Count custom files in bmad-agent
|
||||
const bmadAgentFiles = await glob('**/*.md', {
|
||||
const bmadAgentFiles = await glob("**/*.md", {
|
||||
cwd: bmadAgentPath,
|
||||
ignore: ['node_modules/**'],
|
||||
ignore: ["node_modules/**"],
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -247,233 +268,279 @@ class V3ToV4Upgrader {
|
||||
}
|
||||
|
||||
async showPreflightCheck(analysis, options) {
|
||||
console.log(chalk.bold('\nProject Analysis:'));
|
||||
console.log(chalk.bold("\nProject Analysis:"));
|
||||
console.log(
|
||||
`- PRD found: ${analysis.prdFile ? `docs/${analysis.prdFile}` : chalk.yellow('Not found')}`,
|
||||
`- PRD found: ${
|
||||
analysis.prdFile
|
||||
? `docs/${analysis.prdFile}`
|
||||
: chalk.yellow("Not found")
|
||||
}`
|
||||
);
|
||||
console.log(
|
||||
`- Architecture found: ${
|
||||
analysis.archFile ? `docs/${analysis.archFile}` : chalk.yellow('Not found')
|
||||
}`,
|
||||
analysis.archFile
|
||||
? `docs/${analysis.archFile}`
|
||||
: chalk.yellow("Not found")
|
||||
}`
|
||||
);
|
||||
if (analysis.frontEndArchFile) {
|
||||
console.log(`- Front-end Architecture found: docs/${analysis.frontEndArchFile}`);
|
||||
console.log(
|
||||
`- Front-end Architecture found: docs/${analysis.frontEndArchFile}`
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`- UX/UI Spec found: ${
|
||||
analysis.uxSpecFile ? `docs/${analysis.uxSpecFile}` : chalk.yellow('Not found')
|
||||
}`,
|
||||
analysis.uxSpecFile
|
||||
? `docs/${analysis.uxSpecFile}`
|
||||
: chalk.yellow("Not found")
|
||||
}`
|
||||
);
|
||||
console.log(
|
||||
`- UX/Design Prompt found: ${
|
||||
analysis.uxPromptFile ? `docs/${analysis.uxPromptFile}` : chalk.yellow('Not found')
|
||||
}`,
|
||||
analysis.uxPromptFile
|
||||
? `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}`);
|
||||
|
||||
if (!options.dryRun) {
|
||||
console.log('\nThe following will be backed up to .bmad-v3-backup/:');
|
||||
console.log('- bmad-agent/ (entire directory)');
|
||||
console.log('- docs/ (entire directory)');
|
||||
console.log("\nThe following will be backed up to .bmad-v3-backup/:");
|
||||
console.log("- bmad-agent/ (entire directory)");
|
||||
console.log("- docs/ (entire directory)");
|
||||
|
||||
if (analysis.epicFiles.length > 0) {
|
||||
console.log(
|
||||
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(
|
||||
chalk.green("Since epic files exist, you won't need to shard the PRD after upgrade."),
|
||||
chalk.green(
|
||||
"Since epic files exist, you won't need to shard the PRD after upgrade."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createBackup(projectPath) {
|
||||
const spinner = ora('Creating backup...').start();
|
||||
const spinner = ora("Creating backup...").start();
|
||||
|
||||
try {
|
||||
const backupPath = path.join(projectPath, '.bmad-v3-backup');
|
||||
const backupPath = path.join(projectPath, ".bmad-v3-backup");
|
||||
|
||||
// Check if backup already exists
|
||||
if (await this.pathExists(backupPath)) {
|
||||
spinner.fail('Backup directory already exists');
|
||||
console.error(chalk.red('\nError: Backup directory .bmad-v3-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');
|
||||
spinner.fail("Backup directory already exists");
|
||||
console.error(
|
||||
chalk.red(
|
||||
"\nError: Backup directory .bmad-v3-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
|
||||
await fs.mkdir(backupPath, { recursive: true });
|
||||
spinner.text = '✓ Created .bmad-v3-backup/';
|
||||
console.log(chalk.green('\n✓ Created .bmad-v3-backup/'));
|
||||
spinner.text = "✓ Created .bmad-v3-backup/";
|
||||
console.log(chalk.green("\n✓ Created .bmad-v3-backup/"));
|
||||
|
||||
// Move bmad-agent
|
||||
const bmadAgentSource = path.join(projectPath, 'bmad-agent');
|
||||
const bmadAgentDestination = path.join(backupPath, 'bmad-agent');
|
||||
await fs.rename(bmadAgentSource, bmadAgentDestination);
|
||||
console.log(chalk.green('✓ Moved bmad-agent/ to backup'));
|
||||
const bmadAgentSrc = path.join(projectPath, "bmad-agent");
|
||||
const bmadAgentDest = path.join(backupPath, "bmad-agent");
|
||||
await fs.rename(bmadAgentSrc, bmadAgentDest);
|
||||
console.log(chalk.green("✓ Moved bmad-agent/ to backup"));
|
||||
|
||||
// Move docs
|
||||
const docsSrc = path.join(projectPath, 'docs');
|
||||
const docsDest = path.join(backupPath, 'docs');
|
||||
const docsSrc = path.join(projectPath, "docs");
|
||||
const docsDest = path.join(backupPath, "docs");
|
||||
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) {
|
||||
spinner.fail('Backup failed');
|
||||
spinner.fail("Backup failed");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async installV4Structure(projectPath) {
|
||||
const spinner = ora('Installing V4 structure...').start();
|
||||
const spinner = ora("Installing V4 structure...").start();
|
||||
|
||||
try {
|
||||
// Get the source bmad-core directory (without dot prefix)
|
||||
const sourcePath = path.join(__dirname, '..', '..', 'bmad-core');
|
||||
const destinationPath = path.join(projectPath, '.bmad-core');
|
||||
const sourcePath = path.join(__dirname, "..", "..", "bmad-core");
|
||||
const destPath = path.join(projectPath, ".bmad-core");
|
||||
|
||||
// Copy .bmad-core
|
||||
await this.copyDirectory(sourcePath, destinationPath);
|
||||
spinner.text = '✓ Copied fresh .bmad-core/ directory from V4';
|
||||
console.log(chalk.green('\n✓ Copied fresh .bmad-core/ directory from V4'));
|
||||
await this.copyDirectory(sourcePath, destPath);
|
||||
spinner.text = "✓ Copied fresh .bmad-core/ directory from V4";
|
||||
console.log(
|
||||
chalk.green("\n✓ Copied fresh .bmad-core/ directory from V4")
|
||||
);
|
||||
|
||||
// Create docs directory
|
||||
const docsPath = path.join(projectPath, 'docs');
|
||||
const docsPath = path.join(projectPath, "docs");
|
||||
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
|
||||
await this.createInstallManifest(projectPath);
|
||||
console.log(chalk.green('✓ Created install manifest'));
|
||||
console.log(chalk.green("✓ Created install manifest"));
|
||||
|
||||
console.log(
|
||||
chalk.yellow('\nNote: Your V3 bmad-agent content has been backed up and NOT migrated.'),
|
||||
chalk.yellow(
|
||||
"\nNote: Your V3 bmad-agent content has been backed up and NOT migrated."
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
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) {
|
||||
spinner.fail('V4 installation failed');
|
||||
spinner.fail("V4 installation failed");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async migrateDocuments(projectPath, analysis) {
|
||||
const spinner = ora('Migrating your project documents...').start();
|
||||
const spinner = ora("Migrating your project documents...").start();
|
||||
|
||||
try {
|
||||
const backupDocsPath = path.join(projectPath, '.bmad-v3-backup', 'docs');
|
||||
const newDocsPath = path.join(projectPath, 'docs');
|
||||
const backupDocsPath = path.join(projectPath, ".bmad-v3-backup", "docs");
|
||||
const newDocsPath = path.join(projectPath, "docs");
|
||||
let copiedCount = 0;
|
||||
|
||||
// Copy PRD
|
||||
if (analysis.prdFile) {
|
||||
const source = path.join(backupDocsPath, analysis.prdFile);
|
||||
const destination = path.join(newDocsPath, analysis.prdFile);
|
||||
await fs.copyFile(source, destination);
|
||||
const src = path.join(backupDocsPath, analysis.prdFile);
|
||||
const dest = path.join(newDocsPath, analysis.prdFile);
|
||||
await fs.copyFile(src, dest);
|
||||
console.log(chalk.green(`\n✓ Copied PRD to docs/${analysis.prdFile}`));
|
||||
copiedCount++;
|
||||
}
|
||||
|
||||
// Copy Architecture
|
||||
if (analysis.archFile) {
|
||||
const source = path.join(backupDocsPath, analysis.archFile);
|
||||
const destination = path.join(newDocsPath, analysis.archFile);
|
||||
await fs.copyFile(source, destination);
|
||||
console.log(chalk.green(`✓ Copied Architecture to docs/${analysis.archFile}`));
|
||||
const src = path.join(backupDocsPath, analysis.archFile);
|
||||
const dest = path.join(newDocsPath, analysis.archFile);
|
||||
await fs.copyFile(src, dest);
|
||||
console.log(
|
||||
chalk.green(`✓ Copied Architecture to docs/${analysis.archFile}`)
|
||||
);
|
||||
copiedCount++;
|
||||
}
|
||||
|
||||
// Copy Front-end Architecture if exists
|
||||
if (analysis.frontEndArchFile) {
|
||||
const source = path.join(backupDocsPath, analysis.frontEndArchFile);
|
||||
const destination = path.join(newDocsPath, analysis.frontEndArchFile);
|
||||
await fs.copyFile(source, destination);
|
||||
const src = path.join(backupDocsPath, analysis.frontEndArchFile);
|
||||
const dest = path.join(newDocsPath, analysis.frontEndArchFile);
|
||||
await fs.copyFile(src, dest);
|
||||
console.log(
|
||||
chalk.green(`✓ Copied Front-end Architecture to docs/${analysis.frontEndArchFile}`),
|
||||
chalk.green(
|
||||
`✓ Copied Front-end Architecture to docs/${analysis.frontEndArchFile}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
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++;
|
||||
}
|
||||
|
||||
// Copy UX/UI Spec if exists
|
||||
if (analysis.uxSpecFile) {
|
||||
const source = path.join(backupDocsPath, analysis.uxSpecFile);
|
||||
const destination = path.join(newDocsPath, analysis.uxSpecFile);
|
||||
await fs.copyFile(source, destination);
|
||||
console.log(chalk.green(`✓ Copied UX/UI Spec to docs/${analysis.uxSpecFile}`));
|
||||
const src = path.join(backupDocsPath, analysis.uxSpecFile);
|
||||
const dest = path.join(newDocsPath, analysis.uxSpecFile);
|
||||
await fs.copyFile(src, dest);
|
||||
console.log(
|
||||
chalk.green(`✓ Copied UX/UI Spec to docs/${analysis.uxSpecFile}`)
|
||||
);
|
||||
copiedCount++;
|
||||
}
|
||||
|
||||
// Copy UX/Design Prompt if exists
|
||||
if (analysis.uxPromptFile) {
|
||||
const source = path.join(backupDocsPath, analysis.uxPromptFile);
|
||||
const destination = path.join(newDocsPath, analysis.uxPromptFile);
|
||||
await fs.copyFile(source, destination);
|
||||
console.log(chalk.green(`✓ Copied UX/Design Prompt to docs/${analysis.uxPromptFile}`));
|
||||
const src = path.join(backupDocsPath, analysis.uxPromptFile);
|
||||
const dest = path.join(newDocsPath, analysis.uxPromptFile);
|
||||
await fs.copyFile(src, dest);
|
||||
console.log(
|
||||
chalk.green(
|
||||
`✓ Copied UX/Design Prompt to docs/${analysis.uxPromptFile}`
|
||||
)
|
||||
);
|
||||
copiedCount++;
|
||||
}
|
||||
|
||||
// Copy stories
|
||||
if (analysis.storyFiles.length > 0) {
|
||||
const storiesDir = path.join(newDocsPath, 'stories');
|
||||
const storiesDir = path.join(newDocsPath, "stories");
|
||||
await fs.mkdir(storiesDir, { recursive: true });
|
||||
|
||||
for (const storyFile of analysis.storyFiles) {
|
||||
const source = path.join(backupDocsPath, 'stories', storyFile);
|
||||
const destination = path.join(storiesDir, storyFile);
|
||||
await fs.copyFile(source, destination);
|
||||
const src = path.join(backupDocsPath, "stories", storyFile);
|
||||
const dest = path.join(storiesDir, storyFile);
|
||||
await fs.copyFile(src, dest);
|
||||
}
|
||||
console.log(
|
||||
chalk.green(`✓ Copied ${analysis.storyFiles.length} story files to docs/stories/`),
|
||||
chalk.green(
|
||||
`✓ Copied ${analysis.storyFiles.length} story files to docs/stories/`
|
||||
)
|
||||
);
|
||||
copiedCount += analysis.storyFiles.length;
|
||||
}
|
||||
|
||||
// Copy epic files to prd subfolder
|
||||
if (analysis.epicFiles.length > 0) {
|
||||
const prdDir = path.join(newDocsPath, 'prd');
|
||||
const prdDir = path.join(newDocsPath, "prd");
|
||||
await fs.mkdir(prdDir, { recursive: true });
|
||||
|
||||
for (const epicFile of analysis.epicFiles) {
|
||||
const source = path.join(backupDocsPath, epicFile);
|
||||
const destination = path.join(prdDir, epicFile);
|
||||
await fs.copyFile(source, destination);
|
||||
const src = path.join(backupDocsPath, epicFile);
|
||||
const dest = path.join(prdDir, epicFile);
|
||||
await fs.copyFile(src, dest);
|
||||
}
|
||||
console.log(
|
||||
chalk.green(`✓ Found and copied ${analysis.epicFiles.length} epic files to docs/prd/`),
|
||||
chalk.green(
|
||||
`✓ Found and copied ${analysis.epicFiles.length} epic files to docs/prd/`
|
||||
)
|
||||
);
|
||||
|
||||
// Create index.md for the prd folder
|
||||
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(
|
||||
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;
|
||||
}
|
||||
|
||||
spinner.succeed(`Migrated ${copiedCount} documents successfully`);
|
||||
} catch (error) {
|
||||
spinner.fail('Document migration failed');
|
||||
spinner.fail("Document migration failed");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -481,21 +548,21 @@ class V3ToV4Upgrader {
|
||||
async setupIDE(projectPath, selectedIdes) {
|
||||
// Use the IDE selections passed from the installer
|
||||
if (!selectedIdes || selectedIdes.length === 0) {
|
||||
console.log(chalk.dim('No IDE setup requested - skipping'));
|
||||
console.log(chalk.dim("No IDE setup requested - skipping"));
|
||||
return;
|
||||
}
|
||||
|
||||
const ideSetup = require('../installer/lib/ide-setup');
|
||||
const spinner = ora('Setting up IDE rules for all agents...').start();
|
||||
const ideSetup = require("../installer/lib/ide-setup");
|
||||
const spinner = ora("Setting up IDE rules for all agents...").start();
|
||||
|
||||
try {
|
||||
const ideMessages = {
|
||||
cursor: 'Rules created in .cursor/rules/bmad/',
|
||||
'claude-code': 'Commands created in .claude/commands/BMad/',
|
||||
windsurf: 'Rules created in .windsurf/workflows/',
|
||||
trae: 'Rules created in.trae/rules/',
|
||||
roo: 'Custom modes created in .roomodes',
|
||||
cline: 'Rules created in .clinerules/',
|
||||
cursor: "Rules created in .cursor/rules/bmad/",
|
||||
"claude-code": "Commands created in .claude/commands/BMad/",
|
||||
windsurf: "Rules created in .windsurf/rules/",
|
||||
trae: "Rules created in.trae/rules/",
|
||||
roo: "Custom modes created in .roomodes",
|
||||
cline: "Rules created in .clinerules/",
|
||||
};
|
||||
|
||||
// Setup each selected IDE
|
||||
@@ -506,15 +573,17 @@ class V3ToV4Upgrader {
|
||||
}
|
||||
|
||||
spinner.succeed(`IDE setup complete for ${selectedIdes.length} IDE(s)!`);
|
||||
} catch {
|
||||
spinner.fail('IDE setup failed');
|
||||
console.error(chalk.yellow('IDE setup failed, but upgrade is complete.'));
|
||||
} catch (error) {
|
||||
spinner.fail("IDE setup failed");
|
||||
console.error(
|
||||
chalk.yellow("IDE setup failed, but upgrade is complete.")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
showCompletionReport(projectPath, analysis) {
|
||||
console.log(chalk.bold.green('\n✓ Upgrade Complete!\n'));
|
||||
console.log(chalk.bold('Summary:'));
|
||||
console.log(chalk.bold.green("\n✓ Upgrade Complete!\n"));
|
||||
console.log(chalk.bold("Summary:"));
|
||||
console.log(`- V3 files backed up to: .bmad-v3-backup/`);
|
||||
console.log(`- V4 structure installed: .bmad-core/ (fresh from V4)`);
|
||||
|
||||
@@ -527,36 +596,50 @@ class V3ToV4Upgrader {
|
||||
analysis.storyFiles.length;
|
||||
console.log(
|
||||
`- Documents migrated: ${totalDocs} files${
|
||||
analysis.epicFiles.length > 0 ? ` + ${analysis.epicFiles.length} epics` : ''
|
||||
}`,
|
||||
analysis.epicFiles.length > 0
|
||||
? ` + ${analysis.epicFiles.length} epics`
|
||||
: ""
|
||||
}`
|
||||
);
|
||||
|
||||
console.log(chalk.bold('\nImportant Changes:'));
|
||||
console.log('- 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(chalk.bold("\nImportant Changes:"));
|
||||
console.log(
|
||||
"- 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)"
|
||||
);
|
||||
if (analysis.epicFiles.length > 0) {
|
||||
console.log('- Epic files were found and copied - no PRD sharding needed!');
|
||||
console.log(
|
||||
"- Epic files were found and copied - no PRD sharding needed!"
|
||||
);
|
||||
}
|
||||
if (analysis.frontEndArchFile) {
|
||||
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) {
|
||||
console.log('- UX/UI design files found and copied - ready for use with V4');
|
||||
console.log(
|
||||
"- UX/UI design files found and copied - ready for use with V4"
|
||||
);
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\nNext Steps:'));
|
||||
console.log('1. Review your documents in the new docs/ folder');
|
||||
console.log(chalk.bold("\nNext Steps:"));
|
||||
console.log("1. Review your documents in the new docs/ folder");
|
||||
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) {
|
||||
console.log('3. Use @bmad-master agent to shard the PRD to create epic files');
|
||||
console.log(
|
||||
"3. Use @bmad-master agent to shard the PRD to create epic files"
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.dim('\nYour V3 backup is preserved in .bmad-v3-backup/ and can be restored if needed.'),
|
||||
chalk.dim(
|
||||
"\nYour V3 backup is preserved in .bmad-v3-backup/ and can be restored if needed."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -569,61 +652,67 @@ class V3ToV4Upgrader {
|
||||
}
|
||||
}
|
||||
|
||||
async copyDirectory(source, destination) {
|
||||
await fs.mkdir(destination, { recursive: true });
|
||||
const entries = await fs.readdir(source, { withFileTypes: true });
|
||||
async copyDirectory(src, dest) {
|
||||
await fs.mkdir(dest, { recursive: true });
|
||||
const entries = await fs.readdir(src, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const sourcePath = path.join(source, entry.name);
|
||||
const destinationPath = path.join(destination, entry.name);
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
|
||||
await (entry.isDirectory()
|
||||
? this.copyDirectory(sourcePath, destinationPath)
|
||||
: fs.copyFile(sourcePath, destinationPath));
|
||||
if (entry.isDirectory()) {
|
||||
await this.copyDirectory(srcPath, destPath);
|
||||
} else {
|
||||
await fs.copyFile(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createPrdIndex(projectPath, analysis) {
|
||||
const prdIndexPath = path.join(projectPath, 'docs', 'prd', 'index.md');
|
||||
const prdPath = path.join(projectPath, 'docs', analysis.prdFile || 'prd.md');
|
||||
const prdIndexPath = path.join(projectPath, "docs", "prd", "index.md");
|
||||
const prdPath = path.join(
|
||||
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
|
||||
if (analysis.prdFile && (await this.pathExists(prdPath))) {
|
||||
try {
|
||||
const prdContent = await fs.readFile(prdPath, 'utf8');
|
||||
const lines = prdContent.split('\n');
|
||||
const prdContent = await fs.readFile(prdPath, "utf8");
|
||||
const lines = prdContent.split("\n");
|
||||
|
||||
// Find the first heading
|
||||
const titleMatch = lines.find((line) => line.startsWith('# '));
|
||||
const titleMatch = lines.find((line) => line.startsWith("# "));
|
||||
if (titleMatch) {
|
||||
indexContent = titleMatch + '\n\n';
|
||||
indexContent = titleMatch + "\n\n";
|
||||
}
|
||||
|
||||
// Get any content before the first ## section
|
||||
let introContent = '';
|
||||
let introContent = "";
|
||||
let foundFirstSection = false;
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('## ')) {
|
||||
if (line.startsWith("## ")) {
|
||||
foundFirstSection = true;
|
||||
break;
|
||||
}
|
||||
if (!line.startsWith('# ')) {
|
||||
introContent += line + '\n';
|
||||
if (!line.startsWith("# ")) {
|
||||
introContent += line + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (introContent.trim()) {
|
||||
indexContent += introContent.trim() + '\n\n';
|
||||
indexContent += introContent.trim() + "\n\n";
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// If we can't read the PRD, just use default content
|
||||
}
|
||||
}
|
||||
|
||||
// Add sections list
|
||||
indexContent += '## Sections\n\n';
|
||||
indexContent += "## Sections\n\n";
|
||||
|
||||
// Sort epic files for consistent ordering
|
||||
const sortedEpics = [...analysis.epicFiles].sort();
|
||||
@@ -631,36 +720,38 @@ class V3ToV4Upgrader {
|
||||
for (const epicFile of sortedEpics) {
|
||||
// Extract epic name from filename
|
||||
const epicName = epicFile
|
||||
.replace(/\.md$/, '')
|
||||
.replace(/^epic-?/i, '')
|
||||
.replaceAll('-', ' ')
|
||||
.replace(/^\d+\s*/, '') // Remove leading numbers
|
||||
.replace(/\.md$/, "")
|
||||
.replace(/^epic-?/i, "")
|
||||
.replace(/-/g, " ")
|
||||
.replace(/^\d+\s*/, "") // Remove leading numbers
|
||||
.trim();
|
||||
|
||||
const displayName = epicName.charAt(0).toUpperCase() + epicName.slice(1);
|
||||
indexContent += `- [${displayName || epicFile.replace('.md', '')}](./${epicFile})\n`;
|
||||
indexContent += `- [${
|
||||
displayName || epicFile.replace(".md", "")
|
||||
}](./${epicFile})\n`;
|
||||
}
|
||||
|
||||
await fs.writeFile(prdIndexPath, indexContent);
|
||||
}
|
||||
|
||||
async createInstallManifest(projectPath) {
|
||||
const fileManager = require('../installer/lib/file-manager');
|
||||
const { glob } = require('glob');
|
||||
const fileManager = require("../installer/lib/file-manager");
|
||||
const { glob } = require("glob");
|
||||
|
||||
// Get all files in .bmad-core for the manifest
|
||||
const bmadCorePath = path.join(projectPath, '.bmad-core');
|
||||
const files = await glob('**/*', {
|
||||
const bmadCorePath = path.join(projectPath, ".bmad-core");
|
||||
const files = await glob("**/*", {
|
||||
cwd: bmadCorePath,
|
||||
nodir: true,
|
||||
ignore: ['**/.git/**', '**/node_modules/**'],
|
||||
ignore: ["**/.git/**", "**/node_modules/**"],
|
||||
});
|
||||
|
||||
// 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 = {
|
||||
installType: 'full',
|
||||
installType: "full",
|
||||
agent: null,
|
||||
ide: null, // Will be set if IDE setup is done later
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const fs = require('node:fs');
|
||||
const { execSync } = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
// Dynamic import for ES module
|
||||
let chalk;
|
||||
@@ -31,35 +33,18 @@ async function bumpVersion(type = 'patch') {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const currentVersion = getCurrentVersion();
|
||||
const versionParts = currentVersion.split('.').map(Number);
|
||||
let newVersion;
|
||||
console.log(chalk.yellow('⚠️ Manual version bumping is disabled.'));
|
||||
console.log(chalk.blue('🤖 This project uses semantic-release for automated versioning.'));
|
||||
console.log('');
|
||||
console.log(chalk.bold('To create a new release, use conventional commits:'));
|
||||
console.log(chalk.cyan(' feat: new feature (minor version bump)'));
|
||||
console.log(chalk.cyan(' fix: bug fix (patch version bump)'));
|
||||
console.log(chalk.cyan(' feat!: breaking change (major version bump)'));
|
||||
console.log('');
|
||||
console.log(chalk.dim('Example: git commit -m "feat: add new installer features"'));
|
||||
console.log(chalk.dim('Then push to main branch to trigger automatic release.'));
|
||||
|
||||
switch (type) {
|
||||
case 'major': {
|
||||
newVersion = `${versionParts[0] + 1}.0.0`;
|
||||
break;
|
||||
}
|
||||
case 'minor': {
|
||||
newVersion = `${versionParts[0]}.${versionParts[1] + 1}.0`;
|
||||
break;
|
||||
}
|
||||
case 'patch': {
|
||||
newVersion = `${versionParts[0]}.${versionParts[1]}.${versionParts[2] + 1}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`Bumping version: ${currentVersion} → ${newVersion}`));
|
||||
|
||||
// Update package.json
|
||||
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
packageJson.version = newVersion;
|
||||
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
|
||||
|
||||
console.log(chalk.green(`✓ Updated package.json to ${newVersion}`));
|
||||
|
||||
return newVersion;
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -73,7 +58,7 @@ async function main() {
|
||||
// Check if working directory is clean
|
||||
try {
|
||||
execSync('git diff-index --quiet HEAD --');
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌ Working directory is not clean. Commit your changes first.'));
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -85,7 +70,7 @@ async function main() {
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
main().catch(error => {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const { execSync } = require('node:child_process');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Dynamic import for ES module
|
||||
let chalk;
|
||||
@@ -24,32 +26,25 @@ async function formatYamlContent(content, filename) {
|
||||
// First try to fix common YAML issues
|
||||
let fixedContent = content
|
||||
// Fix "commands :" -> "commands:"
|
||||
.replaceAll(/^(\s*)(\w+)\s+:/gm, '$1$2:')
|
||||
.replace(/^(\s*)(\w+)\s+:/gm, '$1$2:')
|
||||
// Fix inconsistent list indentation
|
||||
.replaceAll(/^(\s*)-\s{3,}/gm, '$1- ');
|
||||
.replace(/^(\s*)-\s{3,}/gm, '$1- ');
|
||||
|
||||
// Skip auto-fixing for .roomodes files - they have special nested structure
|
||||
if (!filename.includes('.roomodes')) {
|
||||
fixedContent = fixedContent
|
||||
// Fix unquoted list items that contain special characters or multiple parts
|
||||
.replaceAll(/^(\s*)-\s+(.*)$/gm, (match, indent, content) => {
|
||||
.replace(/^(\s*)-\s+(.*)$/gm, (match, indent, content) => {
|
||||
// Skip if already quoted
|
||||
if (content.startsWith('"') && content.endsWith('"')) {
|
||||
return match;
|
||||
}
|
||||
// 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")
|
||||
if (
|
||||
(content.includes(':') ||
|
||||
content.includes('-') ||
|
||||
content.includes('{') ||
|
||||
content.includes('}')) &&
|
||||
!/^\w+:\s/.test(content)
|
||||
) {
|
||||
if ((content.includes(':') || content.includes('-') || content.includes('{') || content.includes('}')) &&
|
||||
!content.match(/^\w+:\s/)) {
|
||||
// Remove any existing quotes first, escape internal quotes, then add proper quotes
|
||||
const cleanContent = content
|
||||
.replaceAll(/^["']|["']$/g, '')
|
||||
.replaceAll('"', String.raw`\"`);
|
||||
const cleanContent = content.replace(/^["']|["']$/g, '').replace(/"/g, '\\"');
|
||||
return `${indent}- "${cleanContent}"`;
|
||||
}
|
||||
return match;
|
||||
@@ -67,7 +62,7 @@ async function formatYamlContent(content, filename) {
|
||||
indent: 2,
|
||||
lineWidth: -1, // Disable line wrapping
|
||||
noRefs: true,
|
||||
sortKeys: false, // Preserve key order
|
||||
sortKeys: false // Preserve key order
|
||||
});
|
||||
return formatted;
|
||||
} catch (error) {
|
||||
@@ -85,7 +80,7 @@ async function processMarkdownFile(filePath) {
|
||||
|
||||
// Fix untyped code blocks by adding 'text' type
|
||||
// Match ``` at start of line followed by newline, but only if it's an opening fence
|
||||
newContent = newContent.replaceAll(/^```\n([\s\S]*?)\n```$/gm, '```text\n$1\n```');
|
||||
newContent = newContent.replace(/^```\n([\s\S]*?)\n```$/gm, '```text\n$1\n```');
|
||||
if (newContent !== content) {
|
||||
modified = true;
|
||||
console.log(chalk.blue(`🔧 Added 'text' type to untyped code blocks in ${filePath}`));
|
||||
@@ -111,14 +106,14 @@ async function processMarkdownFile(filePath) {
|
||||
replacements.push({
|
||||
start: match.index,
|
||||
end: match.index + fullMatch.length,
|
||||
replacement: `\`\`\`yaml\n${trimmedFormatted}\n\`\`\``,
|
||||
replacement: `\`\`\`yaml\n${trimmedFormatted}\n\`\`\``
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply replacements in reverse order to maintain indices
|
||||
for (let index = replacements.length - 1; index >= 0; index--) {
|
||||
const { start, end, replacement } = replacements[index];
|
||||
for (let i = replacements.length - 1; i >= 0; i--) {
|
||||
const { start, end, replacement } = replacements[i];
|
||||
newContent = newContent.slice(0, start) + replacement + newContent.slice(end);
|
||||
}
|
||||
|
||||
@@ -160,10 +155,10 @@ async function lintYamlFile(filePath) {
|
||||
|
||||
async function main() {
|
||||
await initializeModules();
|
||||
const arguments_ = process.argv.slice(2);
|
||||
const args = process.argv.slice(2);
|
||||
const glob = require('glob');
|
||||
|
||||
if (arguments_.length === 0) {
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: node yaml-format.js <file1> [file2] ...');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -174,41 +169,35 @@ async function main() {
|
||||
|
||||
// Expand glob patterns and collect all files
|
||||
const allFiles = [];
|
||||
for (const argument of arguments_) {
|
||||
if (argument.includes('*')) {
|
||||
for (const arg of args) {
|
||||
if (arg.includes('*')) {
|
||||
// It's a glob pattern
|
||||
const matches = glob.sync(argument);
|
||||
const matches = glob.sync(arg);
|
||||
allFiles.push(...matches);
|
||||
} else {
|
||||
// It's a direct file path
|
||||
allFiles.push(argument);
|
||||
allFiles.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
for (const filePath of allFiles) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
// Skip silently for glob patterns that don't match anything
|
||||
if (!arguments_.some((argument) => argument.includes('*') && filePath === argument)) {
|
||||
if (!args.some(arg => arg.includes('*') && filePath === arg)) {
|
||||
console.error(chalk.red(`❌ File not found: ${filePath}`));
|
||||
hasErrors = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const basename = path.basename(filePath).toLowerCase();
|
||||
|
||||
try {
|
||||
let changed = false;
|
||||
if (extension === '.md') {
|
||||
if (ext === '.md') {
|
||||
changed = await processMarkdownFile(filePath);
|
||||
} else if (
|
||||
extension === '.yaml' ||
|
||||
extension === '.yml' ||
|
||||
basename.includes('roomodes') ||
|
||||
basename.includes('.yaml') ||
|
||||
basename.includes('.yml')
|
||||
) {
|
||||
} else if (ext === '.yaml' || ext === '.yml' || basename.includes('roomodes') || basename.includes('.yaml') || basename.includes('.yml')) {
|
||||
// Handle YAML files and special cases like .roomodes
|
||||
changed = await processYamlFile(filePath);
|
||||
|
||||
@@ -231,10 +220,8 @@ async function main() {
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
console.log(
|
||||
chalk.green(`\n✨ YAML formatting completed! Modified ${filesProcessed.length} files:`),
|
||||
);
|
||||
for (const file of filesProcessed) console.log(chalk.blue(` 📝 ${file}`));
|
||||
console.log(chalk.green(`\n✨ YAML formatting completed! Modified ${filesProcessed.length} files:`));
|
||||
filesProcessed.forEach(file => console.log(chalk.blue(` 📝 ${file}`)));
|
||||
}
|
||||
|
||||
if (hasErrors) {
|
||||
@@ -244,7 +231,7 @@ async function main() {
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
main().catch(error => {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user