diff --git a/.augment/code_review_guidelines.yaml b/.augment/code_review_guidelines.yaml new file mode 100644 index 000000000..02e4f2b95 --- /dev/null +++ b/.augment/code_review_guidelines.yaml @@ -0,0 +1,271 @@ +# Augment Code Review Guidelines for BMAD-METHOD +# https://docs.augmentcode.com/codereview/overview +# Focus: Workflow validation and quality + +file_paths_to_ignore: + # --- Shared baseline: tool configs --- + - ".coderabbit.yaml" + - ".augment/**" + - "eslint.config.mjs" + # --- Shared baseline: build output --- + - "dist/**" + - "build/**" + - "coverage/**" + # --- Shared baseline: vendored/generated --- + - "node_modules/**" + - "**/*.min.js" + - "**/*.generated.*" + - "**/*.bundle.md" + # --- Shared baseline: package metadata --- + - "package-lock.json" + # --- Shared baseline: binary/media --- + - "*.png" + - "*.jpg" + - "*.svg" + # --- Shared baseline: test fixtures --- + - "test/fixtures/**" + - "test/template-test-generator/**" + - "tools/template-test-generator/test-scenarios/**" + # --- Shared baseline: non-project dirs --- + - "_bmad*/**" + - "website/**" + - "z*/**" + - "sample-project/**" + - "test-project-install/**" + # --- Shared baseline: AI assistant dirs --- + - ".claude/**" + - ".codex/**" + - ".agent/**" + - ".agentvibes/**" + - ".kiro/**" + - ".roo/**" + - ".github/chatmodes/**" + # --- Shared baseline: build temp --- + - ".bundler-temp/**" + # --- Shared baseline: generated reports --- + - "**/validation-report-*.md" + - "CHANGELOG.md" + +areas: + # ============================================ + # WORKFLOW STRUCTURE RULES + # ============================================ + workflow_structure: + description: "Workflow folder organization and required components" + globs: + - "src/**/workflows/**" + rules: + - id: "workflow_entry_point_required" + description: "Every workflow folder must have workflow.yaml, workflow.md, or workflow.xml as entry point" + severity: "high" + + - id: "sharded_workflow_steps_folder" + description: "Sharded workflows (using workflow.md) must have steps/ folder with numbered files (step-01-*.md, step-02-*.md)" + severity: "high" + + - id: "standard_workflow_instructions" + description: "Standard workflows using workflow.yaml must include instructions.md for execution guidance" + severity: "medium" + + - id: "workflow_step_limit" + description: "Workflows should have 5-10 steps maximum to prevent context loss in LLM execution" + severity: "medium" + + # ============================================ + # WORKFLOW ENTRY FILE RULES + # ============================================ + workflow_definitions: + description: "Workflow entry files (workflow.yaml, workflow.md, workflow.xml)" + globs: + - "src/**/workflows/**/workflow.yaml" + - "src/**/workflows/**/workflow.md" + - "src/**/workflows/**/workflow.xml" + rules: + - id: "workflow_name_required" + description: "Workflow entry files must define 'name' field in frontmatter or root element" + severity: "high" + + - id: "workflow_description_required" + description: "Workflow entry files must include 'description' explaining the workflow's purpose" + severity: "high" + + - id: "workflow_config_source" + description: "Workflows should reference config_source for variable resolution (e.g., {project-root}/_bmad/module/config.yaml)" + severity: "medium" + + - id: "workflow_installed_path" + description: "Workflows should define installed_path for relative file references within the workflow" + severity: "medium" + + - id: "valid_step_references" + description: "Step file references in workflow entry must point to existing files" + severity: "high" + + # ============================================ + # SHARDED WORKFLOW STEP RULES + # ============================================ + workflow_steps: + description: "Individual step files in sharded workflows" + globs: + - "src/**/workflows/**/steps/step-*.md" + rules: + - id: "step_goal_required" + description: "Each step must clearly state its goal (## STEP GOAL, ## YOUR TASK, or step n='X' goal='...')" + severity: "high" + + - id: "step_mandatory_rules" + description: "Step files should include MANDATORY EXECUTION RULES section with universal agent behavior rules" + severity: "medium" + + - id: "step_context_boundaries" + description: "Step files should define CONTEXT BOUNDARIES explaining available context and limits" + severity: "medium" + + - id: "step_success_metrics" + description: "Step files should include SUCCESS METRICS section with ✅ checkmarks for validation criteria" + severity: "medium" + + - id: "step_failure_modes" + description: "Step files should include FAILURE MODES section with ❌ marks for anti-patterns to avoid" + severity: "medium" + + - id: "step_next_step_reference" + description: "Step files should reference the next step file path for sequential execution" + severity: "medium" + + - id: "step_no_forward_loading" + description: "Steps must NOT load future step files until current step completes - just-in-time loading only" + severity: "high" + + - id: "valid_file_references" + description: "File path references using {variable}/filename.md must point to existing files" + severity: "high" + + - id: "step_naming" + description: "Step files must be named step-NN-description.md (e.g., step-01-init.md, step-02-context.md)" + severity: "medium" + + - id: "halt_before_menu" + description: "Steps presenting user menus ([C] Continue, [a] Advanced, etc.) must HALT and wait for response" + severity: "high" + + # ============================================ + # XML WORKFLOW/TASK RULES + # ============================================ + xml_workflows: + description: "XML-based workflows and tasks" + globs: + - "src/**/workflows/**/*.xml" + - "src/**/tasks/**/*.xml" + rules: + - id: "xml_task_id_required" + description: "XML tasks must have unique 'id' attribute on root task element" + severity: "high" + + - id: "xml_llm_instructions" + description: "XML workflows should include section with critical execution instructions for the agent" + severity: "medium" + + - id: "xml_step_numbering" + description: "XML steps should use n='X' attribute for sequential numbering" + severity: "medium" + + - id: "xml_action_tags" + description: "Use for required actions, for user input (must HALT), for jumps, for conditionals" + severity: "medium" + + - id: "xml_ask_must_halt" + description: " tags require agent to HALT and wait for user response before continuing" + severity: "high" + + # ============================================ + # WORKFLOW CONTENT QUALITY + # ============================================ + workflow_content: + description: "Content quality and consistency rules for all workflow files" + globs: + - "src/**/workflows/**/*.md" + - "src/**/workflows/**/*.yaml" + rules: + - id: "communication_language_variable" + description: "Workflows should use {communication_language} variable for agent output language consistency" + severity: "low" + + - id: "path_placeholders_required" + description: "Use path placeholders (e.g. {project-root}, {installed_path}, {output_folder}) instead of hardcoded paths" + severity: "medium" + + - id: "no_time_estimates" + description: "Workflows should NOT include time estimates - AI development speed varies significantly" + severity: "low" + + - id: "facilitator_not_generator" + description: "Workflow agents should act as facilitators (guide user input) not content generators (create without input)" + severity: "medium" + + - id: "no_skip_optimization" + description: "Workflows must execute steps sequentially - no skipping or 'optimizing' step order" + severity: "high" + + # ============================================ + # AGENT DEFINITIONS + # ============================================ + agent_definitions: + description: "Agent YAML configuration files" + globs: + - "src/**/*.agent.yaml" + rules: + - id: "agent_metadata_required" + description: "Agent files must have metadata section with id, name, title, icon, and module" + severity: "high" + + - id: "agent_persona_required" + description: "Agent files must define persona with role, identity, communication_style, and principles" + severity: "high" + + - id: "agent_menu_valid_workflows" + description: "Menu triggers must reference valid workflow paths that exist" + severity: "high" + + # ============================================ + # TEMPLATES + # ============================================ + templates: + description: "Template files for workflow outputs" + globs: + - "src/**/template*.md" + - "src/**/templates/**/*.md" + rules: + - id: "placeholder_syntax" + description: "Use {variable_name} or {{variable_name}} syntax consistently for placeholders" + severity: "medium" + + - id: "template_sections_marked" + description: "Template sections that need generation should be clearly marked (e.g., )" + severity: "low" + + # ============================================ + # DOCUMENTATION + # ============================================ + documentation: + description: "Documentation files" + globs: + - "docs/**/*.md" + - "README.md" + - "CONTRIBUTING.md" + rules: + - id: "valid_internal_links" + description: "Internal markdown links must point to existing files" + severity: "medium" + + # ============================================ + # BUILD TOOLS + # ============================================ + build_tools: + description: "Build scripts and tooling" + globs: + - "tools/**" + rules: + - id: "script_error_handling" + description: "Scripts should handle errors gracefully with proper exit codes" + severity: "medium" diff --git a/.claude/skills/changelog-social/SKILL.md b/.claude/skills/changelog-social/SKILL.md index e28b7abda..42e0bc3cf 100644 --- a/.claude/skills/changelog-social/SKILL.md +++ b/.claude/skills/changelog-social/SKILL.md @@ -1,5 +1,5 @@ --- -name: changelog-social +name: bmad-os-changelog-social description: Generate social media announcements for Discord, Twitter, and LinkedIn from the latest changelog entry. Use when user asks to create release announcements, social posts, or share changelog updates. Reads CHANGELOG.md in current working directory. Reference examples/ for tone and format. disable-model-invocation: true --- @@ -154,7 +154,13 @@ Read the appropriate example file before generating to match the established sty ## Output Format -Present both announcements in clearly labeled sections: +**CRITICAL: ALWAYS write to files** - Create files in `_bmad-output/social/` directory: + +1. `{repo-name}-discord-{version}.md` - Discord announcement +2. `{repo-name}-twitter-{version}.md` - Twitter post +3. `{repo-name}-linkedin-{version}.md` - LinkedIn post (if applicable) + +Also present a preview in the chat: ```markdown ## Discord Announcement @@ -166,4 +172,7 @@ Present both announcements in clearly labeled sections: [paste Twitter content here] ``` +Files created: +- `_bmad-output/social/{filename}` + Offer to make adjustments if the user wants different emphasis, tone, or content. diff --git a/.claude/skills/draft-changelog/SKILL.md b/.claude/skills/draft-changelog/SKILL.md index f25f8c48f..a246e069f 100644 --- a/.claude/skills/draft-changelog/SKILL.md +++ b/.claude/skills/draft-changelog/SKILL.md @@ -1,6 +1,6 @@ --- -name: draft-changelog -description: Analyzes changes since the last release and generates a draft changelog entry +name: bmad-os-draft-changelog +description: Analyzes changes since last release and updates CHANGELOG.md ONLY. Does NOT trigger releases. disable-model-invocation: true --- diff --git a/.claude/skills/draft-changelog/prompts/instructions.md b/.claude/skills/draft-changelog/prompts/instructions.md index a9b98bb8f..ef3feccef 100644 --- a/.claude/skills/draft-changelog/prompts/instructions.md +++ b/.claude/skills/draft-changelog/prompts/instructions.md @@ -1,5 +1,17 @@ # Draft Changelog Execution +## ⚠️ IMPORTANT - READ FIRST + +**This skill ONLY updates CHANGELOG.md. That is its entire purpose.** + +- **DO** update CHANGELOG.md with the new version entry +- **DO** present the draft for user review before editing +- **DO NOT** trigger any GitHub release workflows +- **DO NOT** run any other skills or workflows automatically +- **DO NOT** make any commits + +After the changelog is complete, you may suggest the user can run `/release-module` if they want to proceed with the actual release — but NEVER trigger it yourself. + ## Input Project path (or run from project root) @@ -53,6 +65,18 @@ Guidelines: - Clear, concise language - For breaking changes, clearly indicate impact -## Step 4: Present Draft +## Step 4: Present Draft & Update CHANGELOG.md Show the draft with current version, last tag, commit count, and options to edit/retry. + +When user accepts: +1. Update CHANGELOG.md with the new entry (insert at top, after `# Changelog` header) +2. STOP. That's it. You're done. + +You may optionally suggest: *"When ready, you can run `/release-module` to create the actual release."* + +**DO NOT:** +- Trigger any GitHub workflows +- Run any other skills +- Make any commits +- Do anything beyond updating CHANGELOG.md diff --git a/.claude/skills/gh-triage/SKILL.md b/.claude/skills/gh-triage/SKILL.md index 6a6d7c838..e5688f3ba 100644 --- a/.claude/skills/gh-triage/SKILL.md +++ b/.claude/skills/gh-triage/SKILL.md @@ -1,5 +1,5 @@ --- -name: gh-triage +name: bmad-os-gh-triage description: Fetch all GitHub issues via gh CLI and use AI agents to deeply analyze, cluster, and prioritize issues license: MIT disable-model-invocation: true diff --git a/.claude/skills/release-module/SKILL.md b/.claude/skills/release-module/SKILL.md index 3ba156577..17a718a32 100644 --- a/.claude/skills/release-module/SKILL.md +++ b/.claude/skills/release-module/SKILL.md @@ -1,5 +1,5 @@ --- -name: release-module +name: bmad-os-release-module description: Automates the complete release process for npm modules - version bump, changelog, git tag, npm publish, GitHub release disable-model-invocation: true --- diff --git a/.claude/skills/release-module/prompts/instructions.md b/.claude/skills/release-module/prompts/instructions.md index 39e45ac13..157ce0b33 100644 --- a/.claude/skills/release-module/prompts/instructions.md +++ b/.claude/skills/release-module/prompts/instructions.md @@ -42,26 +42,6 @@ Publish the package. Create release with changelog notes using `gh release create`. -### Step 10: Create Social Announcement - -Create a social media announcement file at `_bmad-output/social/{repo-name}-release.md`. - -Format: -```markdown -# {name} v{version} Released - -## Highlights -{2-3 bullet points of key features/changes} - -## Links -- GitHub: {release-url} -- npm: {npm-url} -``` - -### Step 11: Confirm Completion - -Show npm, GitHub, and social announcement file paths. - ## Error Handling Stop immediately on any step failure. Inform user and suggest fix. diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 58eb549f0..9b7f85774 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -17,21 +17,66 @@ reviews: base_branches: - main path_filters: + # --- Shared baseline: tool configs --- + - "!.coderabbit.yaml" + - "!.augment/**" + - "!eslint.config.mjs" + # --- Shared baseline: build output --- + - "!dist/**" + - "!build/**" + - "!coverage/**" + # --- Shared baseline: vendored/generated --- - "!**/node_modules/**" + - "!**/*.min.js" + - "!**/*.generated.*" + - "!**/*.bundle.md" + # --- Shared baseline: package metadata --- + - "!package-lock.json" + # --- Shared baseline: binary/media --- + - "!*.png" + - "!*.jpg" + - "!*.svg" + # --- Shared baseline: test fixtures --- + - "!test/fixtures/**" + - "!test/template-test-generator/**" + - "!tools/template-test-generator/test-scenarios/**" + # --- Shared baseline: non-project dirs --- + - "!_bmad*/**" + - "!website/**" + - "!z*/**" + - "!sample-project/**" + - "!test-project-install/**" + # --- Shared baseline: AI assistant dirs --- + - "!.claude/**" + - "!.codex/**" + - "!.agent/**" + - "!.agentvibes/**" + - "!.kiro/**" + - "!.roo/**" + - "!.github/chatmodes/**" + # --- Shared baseline: build temp --- + - "!.bundler-temp/**" + # --- Shared baseline: generated reports --- + - "!**/validation-report-*.md" + - "!CHANGELOG.md" path_instructions: - path: "**/*" instructions: | - Focus on inconsistencies, contradictions, edge cases and serious issues. - Avoid commenting on minor issues such as linting, formatting and style issues. - When providing code suggestions, use GitHub's suggestion format: - ```suggestion - - ``` - - path: "**/*.js" - instructions: | - CLI tooling code. Check for: missing error handling on fs operations, - path.join vs string concatenation, proper cleanup in error paths. - Flag any process.exit() without error message. + You are a cynical, jaded reviewer with zero patience for sloppy work. + This PR was submitted by a clueless weasel and you expect to find problems. + Be skeptical of everything. + Look for what's missing, not just what's wrong. + Use a precise, professional tone — no profanity or personal attacks. + + Review with extreme skepticism — assume problems exist. + Find at least 10 issues to fix or improve. + + Do NOT: + - Comment on formatting, linting, or style + - Give "looks good" passes + - Anchor on any specific ruleset — reason freely + + If you find zero issues, re-analyze — this is suspicious. chat: auto_reply: true # Response to mentions in comments, a la @coderabbit review issue_enrichment: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..f7224e188 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## What + + +## Why + + + +## How + +- + +## Testing + diff --git a/.github/workflows/coderabbit-review.yaml b/.github/workflows/coderabbit-review.yaml new file mode 100644 index 000000000..fb284d664 --- /dev/null +++ b/.github/workflows/coderabbit-review.yaml @@ -0,0 +1,22 @@ +name: Trigger CodeRabbit on Ready for Review + +on: + pull_request_target: + types: [ready_for_review] + +jobs: + trigger-review: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Request CodeRabbit review + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: '@coderabbitai review' + }); diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 7e5de881b..e28eac969 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -8,7 +8,7 @@ on: - "docs/**" - "src/modules/*/docs/**" - "website/**" - - "tools/build-docs.js" + - "tools/build-docs.mjs" - ".github/workflows/docs.yaml" workflow_dispatch: diff --git a/.github/workflows/manual-release.yaml b/.github/workflows/manual-release.yaml deleted file mode 100644 index 03f6695f8..000000000 --- a/.github/workflows/manual-release.yaml +++ /dev/null @@ -1,193 +0,0 @@ -name: Manual Release - -on: - workflow_dispatch: - inputs: - version_bump: - description: Version bump type - required: true - default: beta - type: choice - options: - - beta - - alpha - - 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-file: ".nvmrc" - 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: | - case "${{ github.event.inputs.version_bump }}" in - alpha|beta) npm version prerelease --no-git-tag-version --preid=${{ github.event.inputs.version_bump }} ;; - *) npm version ${{ github.event.inputs.version_bump }} --no-git-tag-version ;; - esac - - - 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 - - # TODO: Re-enable web bundles once tools/cli/bundlers/ is restored - # - name: Generate web bundles - # run: npm run bundle - - - 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/bmad-code-org/BMAD-METHOD/compare/${{ steps.version.outputs.previous_tag }}...v${{ steps.version.outputs.new_version }} - EOF - - # Output for GitHub Actions - echo "RELEASE_NOTES<> $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: | - VERSION="${{ steps.version.outputs.new_version }}" - if [[ "$VERSION" == *"alpha"* ]]; then - echo "Publishing alpha prerelease version with --tag alpha" - npm publish --tag alpha - elif [[ "$VERSION" == *"beta"* ]]; then - echo "Publishing beta prerelease version with --tag latest" - npm publish --tag latest - else - echo "Publishing stable version with --tag latest" - npm publish --tag latest - fi - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ steps.version.outputs.new_version }} - name: "BMad Method v${{ steps.version.outputs.new_version }}" - body: | - ${{ steps.release_notes.outputs.RELEASE_NOTES }} - draft: false - prerelease: ${{ contains(steps.version.outputs.new_version, 'alpha') || contains(steps.version.outputs.new_version, 'beta') }} - - - name: Summary - run: | - echo "## 🎉 Successfully released v${{ steps.version.outputs.new_version }}!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 📦 Distribution" >> $GITHUB_STEP_SUMMARY - echo "- **NPM**: Published with @latest tag" >> $GITHUB_STEP_SUMMARY - echo "- **GitHub Release**: https://github.com/bmad-code-org/BMAD-METHOD/releases/tag/v${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ✅ Installation" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY - echo "npx bmad-method@${{ steps.version.outputs.new_version }} install" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 89f4370b4..6165d7818 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,9 @@ z*/ _bmad _bmad-output .clinerules -.augment +# .augment/ is gitignored except tracked config files — add exceptions explicitly +.augment/* +!.augment/code_review_guidelines.yaml .crush .cursor .iflow diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a85c1f33..f28c7f5d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,8 +58,7 @@ "tmpl", "Trae", "Unsharded", - "VNET", - "webskip" + "VNET" ], "json.schemas": [ { diff --git a/CHANGELOG.md b/CHANGELOG.md index 2420869b4..7911a7d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,70 @@ # Changelog +## [6.0.0-Beta.7] + +**Release: February 4, 2026** + +### 🌟 Key Highlights + +1. **Direct Workflow Invocation** — Agent workflows can now be run directly via slash commands instead of only through agent orchestration +2. **Installer Workflow Support** — Installer now picks up `workflow-*.md` files, enabling multiple workflow files per directory + +### 🎁 Features + +* **Slash Command Workflow Access** — Research and PRD workflows now accessible via direct slash commands: `/domain-research`, `/market-research`, `/technical-research`, `/create-prd`, `/edit-prd`, `/validate-prd` (bd620e38, 731bee26) +* **Version Checking** — CLI now checks npm for newer versions and displays a warning banner when updates are available (d37ee7f2) + +### ♻️ Refactoring + +* **Workflow File Splitting** — Split monolithic `workflow.md` files into specific `workflow-*.md` files for individual workflow invocation (bd620e38) +* **Installer Multi-Workflow Support** — Installer manifest generator now supports `workflow-*.md` pattern, allowing multiple workflow files per directory (731bee26) +* **Internal Skill Renaming** — Renamed internal project skills to use `bmad-os-` prefix for consistent naming (5276d58b) + +--- + +## [6.0.0-Beta.6] + +**Release: February 4, 2026** + +### 🌟 Key Highlights + +1. **Cross-File Reference Validator**: Comprehensive tool to detect broken file references, preventing 59 known bugs (~25% of historical issues) +2. **New AutocompleteMultiselect Prompt**: Searchable multi-select with improved tool/IDE selection UX +3. **Critical Installer Fixes**: Windows CRLF parsing, Gemini CLI TOML support, file extension preservation +4. **Codebase Cleanup**: Removed dead Excalidraw/flattener artifacts (-3,798 lines) + +### 🎁 Features + +* **Cross-File Reference Validator** — Validates ~483 references across ~217 source files, detecting absolute path leaks and broken references (PR #1494) +* **AutocompleteMultiselect Prompt** — Upgraded `@clack/prompts` to v1.0.0 with custom searchable multiselect, Tab-to-fill-placeholder behavior, and improved tool/IDE selection UX (PR #1514) +* **OT Domains** — Added `process_control` and `building_automation` domains with high complexity ratings (PR #1510) +* **Documentation Reference Pages** — Added `docs/reference/agents.md`, `commands.md`, and `testing.md` (PR #1525) + +### 🐛 Bug Fixes + +* **Critical Installer Fixes** — Fixed CRLF line ending parsing on Windows, Gemini CLI TOML support, file extension preservation, Codex task generation, Windows path handling, and CSV parsing (PR #1492) +* **Double Tool Questioning** — Removed redundant tool questioning during installation (df176d42) +* **QA Agent Rename** — Renamed Quinn agent to `qa` for naming consistency (PR #1508) +* **Documentation Organization** — Fixed documentation ordering and links, hide BMGD pages from main LLM docs (PR #1525) + +### ♻️ Refactoring + +* **Excalidraw/Flattener Removal** — Removed dead artifacts no longer supported beyond beta: Excalidraw workflows, flattener tool, and 12+ diagram creation workflows (-3,798 lines) (f699a368) +* **Centralized Constants** — Centralized `BMAD_FOLDER_NAME` to reduce hardcoded strings (PR #1492) +* **Cross-Platform Paths** — Fixed path separator inconsistencies in agent IDs (PR #1492) + +### 📚 Documentation + +* **BMGD Diataxis Refactor** — Refactored BMGD documentation using Diataxis principles for better organization (PR #1502) +* **Generate Project Context** — Restored `generate-project-context` workflow for brownfield project analysis (PR #1491) + +### 🔧 Maintenance + +* **Dependency Updates** — Upgraded `@clack/prompts` from v0.11.0 to v1.0.0 and added `@clack/core` (PR #1514) +* **CI Integration** — Added `validate:refs` to CI quality workflow with warning annotations (PR #1494) + +--- + ## [6.0.0-Beta.5] ### 🎁 Features @@ -1201,7 +1266,6 @@ Located in `src/modules/bmb/workflows/agent/data/`: - **Workflow Vendoring**: Web bundler performs automatic cross-module dependency vendoring - **BMGD Module Extraction**: Game development split into standalone 4-phase structure -- **Enhanced Dependency Resolution**: Better handling of web_bundle: false workflows - **Advanced Elicitation Fix**: Added missing CSV files to workflow bundles - **Claude Code Fix**: Resolved README slash command installation regression diff --git a/README.md b/README.md index 25828f395..6e1f3a9b0 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,14 @@ npx bmad-method install Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, Windsurf, etc.) in the project folder. +**Non-Interactive Installation**: For CI/CD pipelines or automated deployments, use command-line flags: + +```bash +npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes +``` + +See [Non-Interactive Installation Guide](docs/non-interactive-installation.md) for all available options. + > **Not sure what to do?** Run `/bmad-help` — it tells you exactly what's next and what's optional. You can also ask it questions like: - `/bmad-help How should I build a web app for my TShirt Business that can scale to millions?` diff --git a/docs/_STYLE_GUIDE.md b/docs/_STYLE_GUIDE.md index e5fb51ff7..801314cd0 100644 --- a/docs/_STYLE_GUIDE.md +++ b/docs/_STYLE_GUIDE.md @@ -1,5 +1,6 @@ --- title: "Documentation Style Guide" +description: Project-specific documentation conventions based on Google style and Diataxis structure --- This project adheres to the [Google Developer Documentation Style Guide](https://developers.google.com/style) and uses [Diataxis](https://diataxis.fr/) to structure content. Only project-specific conventions follow. @@ -147,7 +148,7 @@ your-project/ | **Concept** | `what-are-agents.md` | | **Feature** | `quick-flow.md` | | **Philosophy** | `why-solutioning-matters.md` | -| **FAQ** | `brownfield-faq.md` | +| **FAQ** | `established-projects-faq.md` | ### General Template @@ -325,7 +326,7 @@ Add italic context at definition start for limited-scope terms: - `*BMad Method/Enterprise.*` - `*Phase N.*` - `*BMGD.*` -- `*Brownfield.*` +- `*Established projects.*` ### Glossary Checklist diff --git a/docs/bmgd/game-types.md b/docs/bmgd/game-types.md index ed0a7164c..87870ea10 100644 --- a/docs/bmgd/game-types.md +++ b/docs/bmgd/game-types.md @@ -1,5 +1,7 @@ --- title: "Game Types Reference" +description: 24 game type templates with genre-specific GDD sections for BMGD +draft: true --- BMGD supports 24 game type templates. Each adds genre-specific sections to your GDD. diff --git a/docs/bmgd/index.md b/docs/bmgd/index.md index 08f2f6078..bd1565c12 100644 --- a/docs/bmgd/index.md +++ b/docs/bmgd/index.md @@ -1,6 +1,7 @@ --- title: "BMGD Quick Guide" description: Quick reference for BMad Game Dev Studio +draft: true --- ![BMGD Logo](bmgd-logo.png) @@ -110,4 +111,3 @@ Each template provides genre-specific GDD sections, mechanics patterns, testing - [Game Types Guide](game-types.md) - [Quick-Flow Guide](quick-flow-workflows.md) - diff --git a/docs/bmgd/quick-flow-workflows.md b/docs/bmgd/quick-flow-workflows.md index b86cc7674..239f263a6 100644 --- a/docs/bmgd/quick-flow-workflows.md +++ b/docs/bmgd/quick-flow-workflows.md @@ -1,5 +1,7 @@ --- title: "Quick Flow Workflows" +description: Create tech specs and execute implementations with BMGD Quick Flow +draft: true --- How to create tech specs and execute implementations with Quick Flow. diff --git a/docs/downloads.md b/docs/downloads.md deleted file mode 100644 index d063c8e10..000000000 --- a/docs/downloads.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Downloads ---- - -Download BMad Method resources for offline use, AI training, or integration. - -## Source Bundles - -Download these from the `downloads/` folder on the documentation site. - -| File | Description | -| ------------------ | ------------------------------- | -| `bmad-sources.zip` | Complete BMad source files | -| `bmad-prompts.zip` | Agent and workflow prompts only | - -## LLM-Optimized Files - -These files are designed for AI consumption - perfect for loading into Claude, ChatGPT, or any LLM context window. See [API Access](#api-access) below for URLs. - -| File | Description | Use Case | -| --------------- | ----------------------------------- | -------------------------- | -| `llms.txt` | Documentation index with summaries | Quick overview, navigation | -| `llms-full.txt` | Complete documentation concatenated | Full context loading | - -### Using with LLMs - -**Claude Projects:** -``` -Upload llms-full.txt as project knowledge -``` - -**ChatGPT:** -``` -Paste llms.txt for navigation, or sections from llms-full.txt as needed -``` - -**API Usage:** -```python -import requests -docs = requests.get("https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt").text -# Include in your system prompt or context -``` - -## Installation Options - -```bash -npx bmad-method install -``` - -[More details](/docs/how-to/install-bmad.md) - -## Version Information - -- **Current Version:** See [CHANGELOG](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/CHANGELOG.md) -- **Release Notes:** Available on [GitHub Releases](https://github.com/bmad-code-org/BMAD-METHOD/releases) - -## API Access - -For programmatic access to BMad documentation: - -```bash -# Get documentation index -curl https://bmad-code-org.github.io/BMAD-METHOD/llms.txt - -# Get full documentation -curl https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt -``` - -## Contributing - -Want to improve BMad Method? Check out: - -- [Contributing Guide](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/CONTRIBUTING.md) -- [GitHub Repository](https://github.com/bmad-code-org/BMAD-METHOD) diff --git a/docs/explanation/brownfield-faq.md b/docs/explanation/brownfield-faq.md deleted file mode 100644 index 1c9b3b822..000000000 --- a/docs/explanation/brownfield-faq.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: "Brownfield Development FAQ" -description: Common questions about brownfield development in the BMad Method ---- -Quick answers to common questions about brownfield (existing codebase) development in the BMad Method (BMM). - -## Questions - -- [Questions](#questions) - - [What is brownfield vs greenfield?](#what-is-brownfield-vs-greenfield) - - [Do I have to run document-project for brownfield?](#do-i-have-to-run-document-project-for-brownfield) - - [What if I forget to run document-project?](#what-if-i-forget-to-run-document-project) - - [Can I use Quick Spec Flow for brownfield projects?](#can-i-use-quick-spec-flow-for-brownfield-projects) - - [What if my existing code doesn't follow best practices?](#what-if-my-existing-code-doesnt-follow-best-practices) - -### What is brownfield vs greenfield? - -- **Greenfield** — New project, starting from scratch, clean slate -- **Brownfield** — Existing project, working with established codebase and patterns - -### Do I have to run document-project for brownfield? - -Highly recommended, especially if: - -- No existing documentation -- Documentation is outdated -- AI agents need context about existing code - -You can skip it if you have comprehensive, up-to-date documentation including `docs/index.md` or will use other tools or techniques to aid in discovery for the agent to build on an existing system. - -### What if I forget to run document-project? - -Don't worry about it - you can do it at any time. You can even do it during or after a project to help keep docs up to date. - -### Can I use Quick Spec Flow for brownfield projects? - -Yes! Quick Spec Flow works great for brownfield. It will: - -- Auto-detect your existing stack -- Analyze brownfield code patterns -- Detect conventions and ask for confirmation -- Generate context-rich tech-spec that respects existing code - -Perfect for bug fixes and small features in existing codebases. - -### What if my existing code doesn't follow best practices? - -Quick Spec Flow detects your conventions and asks: "Should I follow these existing conventions?" You decide: - -- **Yes** → Maintain consistency with current codebase -- **No** → Establish new standards (document why in tech-spec) - -BMM respects your choice — it won't force modernization, but it will offer it. - -**Have a question not answered here?** Please [open an issue](https://github.com/bmad-code-org/BMAD-METHOD/issues) or ask in [Discord](https://discord.gg/gk8jAdXWmj) so we can add it! diff --git a/docs/explanation/established-projects-faq.md b/docs/explanation/established-projects-faq.md new file mode 100644 index 000000000..e940b4dbb --- /dev/null +++ b/docs/explanation/established-projects-faq.md @@ -0,0 +1,48 @@ +--- +title: "Established Projects FAQ" +description: Common questions about using BMad Method on established projects +--- +Quick answers to common questions about working on established projects with the BMad Method (BMM). + +## Questions + +- [Do I have to run document-project first?](#do-i-have-to-run-document-project-first) +- [What if I forget to run document-project?](#what-if-i-forget-to-run-document-project) +- [Can I use Quick Flow for established projects?](#can-i-use-quick-flow-for-established-projects) +- [What if my existing code doesn't follow best practices?](#what-if-my-existing-code-doesnt-follow-best-practices) + +### Do I have to run document-project first? + +Highly recommended, especially if: + +- No existing documentation +- Documentation is outdated +- AI agents need context about existing code + +You can skip it if you have comprehensive, up-to-date documentation including `docs/index.md` or will use other tools or techniques to aid in discovery for the agent to build on an existing system. + +### What if I forget to run document-project? + +Don't worry about it - you can do it at any time. You can even do it during or after a project to help keep docs up to date. + +### Can I use Quick Flow for established projects? + +Yes! Quick Flow works great for established projects. It will: + +- Auto-detect your existing stack +- Analyze existing code patterns +- Detect conventions and ask for confirmation +- Generate context-rich tech-spec that respects existing code + +Perfect for bug fixes and small features in existing codebases. + +### What if my existing code doesn't follow best practices? + +Quick Flow detects your conventions and asks: "Should I follow these existing conventions?" You decide: + +- **Yes** → Maintain consistency with current codebase +- **No** → Establish new standards (document why in tech-spec) + +BMM respects your choice — it won't force modernization, but it will offer it. + +**Have a question not answered here?** Please [open an issue](https://github.com/bmad-code-org/BMAD-METHOD/issues) or ask in [Discord](https://discord.gg/gk8jAdXWmj) so we can add it! diff --git a/docs/how-to/customize-bmad.md b/docs/how-to/customize-bmad.md index f23a101c6..3c356373f 100644 --- a/docs/how-to/customize-bmad.md +++ b/docs/how-to/customize-bmad.md @@ -1,5 +1,6 @@ --- title: "BMad Method Customization Guide" +description: Customize agents, workflows, and modules while preserving update compatibility --- The ability to customize the BMad Method and its core to your needs, while still being able to get updates and enhancements is a critical idea within the BMad Ecosystem. diff --git a/docs/how-to/brownfield/index.md b/docs/how-to/established-projects.md similarity index 83% rename from docs/how-to/brownfield/index.md rename to docs/how-to/established-projects.md index 75bab690b..2f362d0b6 100644 --- a/docs/how-to/brownfield/index.md +++ b/docs/how-to/established-projects.md @@ -1,15 +1,11 @@ --- -title: "Brownfield Development" +title: "Established Projects" description: How to use BMad Method on existing codebases --- -Use BMad Method effectively when working on existing projects and legacy codebases. +Use BMad Method effectively when working on existing projects and legacy codebases, sometimes also referred to as brownfield projects. -## What is Brownfield Development? - -**Brownfield** refers to working on existing projects with established codebases and patterns, as opposed to **greenfield** which means starting from scratch with a clean slate. - -This guide covers the essential workflow for onboarding to brownfield projects with BMad Method. +This guide covers the essential workflow for onboarding to existing projects with BMad Method. :::note[Prerequisites] - BMad Method installed (`npx bmad-method install`) @@ -80,5 +76,5 @@ Pay close attention here to prevent reinventing the wheel or making decisions th ## More Information -- **[Quick Fix in Brownfield](/docs/how-to/brownfield/quick-fix-in-brownfield.md)** - Bug fixes and ad-hoc changes -- **[Brownfield FAQ](/docs/explanation/brownfield-faq.md)** - Common questions about brownfield development +- **[Quick Fixes](/docs/how-to/quick-fixes.md)** - Bug fixes and ad-hoc changes +- **[Established Projects FAQ](/docs/explanation/established-projects-faq.md)** - Common questions about working on established projects diff --git a/docs/how-to/get-answers-about-bmad.md b/docs/how-to/get-answers-about-bmad.md index a8dbad09b..e85069be1 100644 --- a/docs/how-to/get-answers-about-bmad.md +++ b/docs/how-to/get-answers-about-bmad.md @@ -42,8 +42,6 @@ Fetch `llms-full.txt` into your session: https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt ``` -See the [Downloads page](/docs/downloads.md) for other downloadable resources. - ### 3. Ask Your Question :::note[Example] diff --git a/docs/how-to/install-bmad.md b/docs/how-to/install-bmad.md index 27e7b253c..0b477905a 100644 --- a/docs/how-to/install-bmad.md +++ b/docs/how-to/install-bmad.md @@ -5,6 +5,8 @@ description: Step-by-step guide to installing BMad in your project Use the `npx bmad-method install` command to set up BMad in your project with your choice of modules and AI tools. +If you want to use a non interactive installer and provide all install options on the command line, [this guide](/docs/non-interactive-installation.md). + ## When to Use This - Starting a new project with BMad diff --git a/docs/how-to/brownfield/quick-fix-in-brownfield.md b/docs/how-to/quick-fixes.md similarity index 93% rename from docs/how-to/brownfield/quick-fix-in-brownfield.md rename to docs/how-to/quick-fixes.md index 9dc430f11..5b6cfe35c 100644 --- a/docs/how-to/brownfield/quick-fix-in-brownfield.md +++ b/docs/how-to/quick-fixes.md @@ -1,6 +1,6 @@ --- -title: "How to Make Quick Fixes in Brownfield Projects" -description: How to make quick fixes and ad-hoc changes in brownfield projects +title: "Quick Fixes" +description: How to make quick fixes and ad-hoc changes --- Use the **DEV agent** directly for bug fixes, refactorings, or small targeted changes that don't require the full BMad method or Quick Flow. diff --git a/docs/how-to/shard-large-documents.md b/docs/how-to/shard-large-documents.md index a8cabc5b3..45925e5c6 100644 --- a/docs/how-to/shard-large-documents.md +++ b/docs/how-to/shard-large-documents.md @@ -1,24 +1,19 @@ --- title: "Document Sharding Guide" +description: Split large markdown files into smaller organized files for better context management --- -Use the `shard-doc` tool to split large markdown files into smaller, organized files for better context management. +Use the `shard-doc` tool if you need to split large markdown files into smaller, organized files for better context management. + +This is no longer recommended, and soon with updated workflows and most major llms and tools supporting sub processes this will be unnecessary. ## When to Use This -- Very large complex PRDs -- Architecture documents with multiple system layers -- Epic files with 4+ epics (especially for Phase 4) -- UX design specs covering multiple subsystems +Only use this if you notice your chosen tool / model combination are failing to load and read all the documents as input when needed. ## What is Document Sharding? -Document sharding splits large markdown files into smaller, organized files based on level 2 headings (`## Heading`). This enables: - -- **Selective Loading** - Workflows load only the sections they need -- **Reduced Token Usage** - Massive efficiency gains for large projects -- **Better Organization** - Logical section-based file structure -- **Maintained Context** - Index file preserves document structure +Document sharding splits large markdown files into smaller, organized files based on level 2 headings (`## Heading`). ### Architecture @@ -61,28 +56,6 @@ Agent: Sharding PRD.md... ✓ Complete! ``` -## What You Get - -**index.md structure:** - -```markdown - -## Sections - -1. [Overview](./overview.md) - Project vision and objectives -2. [User Requirements](./user-requirements.md) - Feature specifications -3. [Epic 1: Authentication](./epic-1-authentication.md) - User auth system -4. [Epic 2: Dashboard](./epic-2-dashboard.md) - Main dashboard UI - ... -``` - -**Individual section files:** - -- Named from heading text (kebab-case) -- Contains complete section content -- Preserves all markdown formatting -- Can be read independently - ## How Workflow Discovery Works BMad workflows use a **dual discovery system**: diff --git a/docs/how-to/upgrade-to-v6.md b/docs/how-to/upgrade-to-v6.md index 3d576f463..e8d13a5e8 100644 --- a/docs/how-to/upgrade-to-v6.md +++ b/docs/how-to/upgrade-to-v6.md @@ -20,14 +20,7 @@ Use the BMad installer to upgrade from v4 to v6, which includes automatic detect ### 1. Run the Installer -```bash -npx bmad-method install -``` - -The installer automatically detects: - -- **Legacy v4 folder**: `.bmad-method` -- **IDE command artifacts**: Legacy bmad folders in `.claude/commands/`, `.cursor/commands/`, etc. +Follow the [Installer Instructions](/docs/how-to/install-bmad.md). ### 2. Handle Legacy Installation @@ -35,21 +28,16 @@ When v4 is detected, you can: - Allow the installer to back up and remove `.bmad-method` - Exit and handle cleanup manually -- Keep both (not recommended for same project) + +If you named your bmad method folder something else - you will need to manual remove the folder yourself. ### 3. Clean Up IDE Commands -Manually remove legacy v4 IDE commands: +Manually remove legacy v4 IDE commands - for example if you have claude, look for any nested folders that start with bmad and remove them: - `.claude/commands/BMad/agents` - `.claude/commands/BMad/tasks` -New v6 commands will be at `.claude/commands/bmad//agents|workflows`. - -:::tip[Accidentally Deleted Commands?] -If you delete the wrong commands, rerun the installer and choose "quick update" to restore them. -::: - ### 4. Migrate Planning Artifacts **If you have planning documents (Brief/PRD/UX/Architecture):** @@ -71,24 +59,6 @@ If you have stories created or implemented: 3. Run the Scrum Master's `sprint-planning` workflow 4. Tell the SM which epics/stories are already complete -### 6. Migrate Agent Customizations - -**v4:** Modified agent files directly in `_bmad-*` folders - -**v6:** All customizations go in `_bmad/_config/agents/` using customize files: - -```yaml -# _bmad/_config/agents/bmm-pm.customize.yaml -persona: - name: 'Captain Jack' - role: 'Swashbuckling Product Owner' - communication_style: | - - Talk like a pirate - - Use nautical metaphors -``` - -After modifying customization files, rerun the installer and choose "rebuild all agents" or "quick update". - ## What You Get **v6 unified structure:** @@ -107,25 +77,19 @@ your-project/ ## Module Migration -| v4 Module | v6 Status | -|-----------|-----------| -| `_bmad-2d-phaser-game-dev` | Integrated into BMGD Module | -| `_bmad-2d-unity-game-dev` | Integrated into BMGD Module | -| `_bmad-godot-game-dev` | Integrated into BMGD Module | -| `_bmad-infrastructure-devops` | Deprecated — new DevOps agent coming soon | -| `_bmad-creative-writing` | Not adapted — new v6 module coming soon | +| v4 Module | v6 Status | +| ----------------------------- | ----------------------------------------- | +| `.bmad-2d-phaser-game-dev` | Integrated into BMGD Module | +| `.bmad-2d-unity-game-dev` | Integrated into BMGD Module | +| `.bmad-godot-game-dev` | Integrated into BMGD Module | +| `.bmad-infrastructure-devops` | Deprecated — new DevOps agent coming soon | +| `.bmad-creative-writing` | Not adapted — new v6 module coming soon | ## Key Changes -| Concept | v4 | v6 | -|---------|----|----| -| **Core** | `_bmad-core` was actually BMad Method | `_bmad/core/` is universal framework | -| **Method** | `_bmad-method` | `_bmad/bmm/` | -| **Config** | Modified files directly | `config.yaml` per module | -| **Documents** | Sharded or unsharded required setup | Fully flexible, auto-scanned | - -## Tips - -- **Back up first** — Keep your v4 installation until you verify v6 works -- **Use v6 workflows** — Even partial planning docs benefit from v6's improved discovery -- **Rebuild after customizing** — Always run the installer after changing customize files +| Concept | v4 | v6 | +| ------------- | ------------------------------------- | ------------------------------------ | +| **Core** | `_bmad-core` was actually BMad Method | `_bmad/core/` is universal framework | +| **Method** | `_bmad-method` | `_bmad/bmm/` | +| **Config** | Modified files directly | `config.yaml` per module | +| **Documents** | Sharded or unsharded required setup | Fully flexible, auto-scanned | diff --git a/docs/index.md b/docs/index.md index 3dcf5cd0e..8b626a63d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,9 @@ --- title: Welcome to the BMad Method +description: AI-driven development framework with specialized agents, guided workflows, and intelligent planning --- -The BMad Method (**B**reakthrough **M**ethod of **A**gile AI **D**riven Development) is an AI-driven development framework that helps you build software faster and smarter. It provides specialized AI agents, guided workflows, and intelligent planning that adapts to your project's complexity—whether you're fixing a bug or building an enterprise platform. +The BMad Method (**B**reakthrough **M**ethod of **A**gile AI **D**riven Development) is an AI-driven development framework that helps you build software through the whole process from ideation and planning all the way through agentic implementation. It provides specialized AI agents, guided workflows, and intelligent planning that adapts to your project's complexity, whether you're fixing a bug or building an enterprise platform. If you're comfortable working with AI coding assistants like Claude, Cursor, or GitHub Copilot, you're ready to get started. diff --git a/docs/non-interactive-installation.md b/docs/non-interactive-installation.md new file mode 100644 index 000000000..7541ecc20 --- /dev/null +++ b/docs/non-interactive-installation.md @@ -0,0 +1,314 @@ +--- +title: Non-Interactive Installation +description: Install BMAD using command-line flags for CI/CD pipelines and automated deployments +--- + +# Non-Interactive Installation + +BMAD now supports non-interactive installation through command-line flags. This is particularly useful for: + +- Automated deployments and CI/CD pipelines +- Scripted installations +- Batch installations across multiple projects +- Quick installations with known configurations + +## Installation Modes + +### 1. Fully Interactive (Default) + +Run without any flags to use the traditional interactive prompts: + +```bash +npx bmad-method install +``` + +### 2. Fully Non-Interactive + +Provide all required flags to skip all prompts: + +```bash +npx bmad-method install \ + --directory /path/to/project \ + --modules bmm,bmb \ + --tools claude-code,cursor \ + --user-name "John Doe" \ + --communication-language English \ + --document-output-language English \ + --output-folder _bmad-output +``` + +### 3. Semi-Interactive (Graceful Fallback) + +Provide some flags and let BMAD prompt for the rest: + +```bash +npx bmad-method install \ + --directory /path/to/project \ + --modules bmm +``` + +In this case, BMAD will: +- Use the provided directory and modules +- Prompt for tool selection +- Prompt for core configuration + +### 4. Quick Install with Defaults + +Use the `-y` or `--yes` flag to accept all defaults: + +```bash +npx bmad-method install --yes +``` + +This will: +- Install to the current directory +- Skip custom content prompts +- Use default values for all configuration +- Use previously configured tools (or skip tool configuration if none exist) + +### 5. Install Without Tools + +To skip tool/IDE configuration entirely: + +**Option 1: Use --tools none** +```bash +npx bmad-method install --directory ~/myapp --modules bmm --tools none +``` + +**Option 2: Use --yes flag (if no tools were previously configured)** +```bash +npx bmad-method install --yes +``` + +**Option 3: Omit --tools and select "None" in the interactive prompt** +```bash +npx bmad-method install --directory ~/myapp --modules bmm +# Then select "⚠ None - I am not installing any tools" when prompted +``` + +## Available Flags + +### Installation Options + +| Flag | Description | Example | +|------|-------------|---------| +| `--directory ` | Installation directory | `--directory ~/projects/myapp` | +| `--modules ` | Comma-separated module IDs | `--modules bmm,bmb` | +| `--tools ` | Comma-separated tool/IDE IDs (use "none" to skip) | `--tools claude-code,cursor` or `--tools none` | +| `--custom-content ` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` | +| `--action ` | Action for existing installations | `--action quick-update` | + +### Core Configuration + +| Flag | Description | Default | +|------|-------------|---------| +| `--user-name ` | Name for agents to use | System username | +| `--communication-language ` | Agent communication language | English | +| `--document-output-language ` | Document output language | English | +| `--output-folder ` | Output folder path | _bmad-output | + +### Other Options + +| Flag | Description | +|------|-------------| +| `-y, --yes` | Accept all defaults and skip prompts | +| `-d, --debug` | Enable debug output for manifest generation | + +## Action Types + +When working with existing installations, use the `--action` flag: + +- `install` - Fresh installation (default for new directories) +- `update` - Modify existing installation (change modules/config) +- `quick-update` - Refresh installation without changing configuration +- `compile-agents` - Recompile agents with customizations only + +Example: + +```bash +npx bmad-method install --action quick-update +``` + +## Module IDs + +Available module IDs for the `--modules` flag: + +### Core Modules +- `bmm` - BMad Method Master +- `bmb` - BMad Builder + +### External Modules +Check the [BMad registry](https://github.com/bmad-code-org) for available external modules. + +## Tool/IDE IDs + +Available tool IDs for the `--tools` flag: + +- `claude-code` - Claude Code CLI +- `cursor` - Cursor IDE +- `windsurf` - Windsurf IDE +- `vscode` - Visual Studio Code +- `jetbrains` - JetBrains IDEs +- And more... + +Run the interactive installer once to see all available tools. + +## Examples + +### Basic Installation + +Install BMM module with Claude Code: + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --modules bmm \ + --tools claude-code \ + --user-name "Development Team" +``` + +### Installation Without Tools + +Install without configuring any tools/IDEs: + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --modules bmm \ + --tools none \ + --user-name "Development Team" +``` + +### Full Installation with Multiple Modules + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --modules bmm,bmb \ + --tools claude-code,cursor \ + --user-name "John Doe" \ + --communication-language English \ + --document-output-language English \ + --output-folder _output +``` + +### Update Existing Installation + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --action update \ + --modules bmm,bmb,custom-module +``` + +### Quick Update (Preserve Settings) + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --action quick-update +``` + +### Installation with Custom Content + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --modules bmm \ + --custom-content ~/my-custom-module,~/another-module \ + --tools claude-code +``` + +### CI/CD Pipeline Installation + +```bash +#!/bin/bash +# install-bmad.sh + +npx bmad-method install \ + --directory "${GITHUB_WORKSPACE}" \ + --modules bmm \ + --tools claude-code \ + --user-name "CI Bot" \ + --communication-language English \ + --document-output-language English \ + --output-folder _bmad-output \ + --yes +``` + +## Environment-Specific Installations + +### Development Environment + +```bash +npx bmad-method install \ + --directory . \ + --modules bmm,bmb \ + --tools claude-code,cursor \ + --user-name "${USER}" +``` + +### Production Environment + +```bash +npx bmad-method install \ + --directory /opt/app \ + --modules bmm \ + --tools claude-code \ + --user-name "Production Team" \ + --output-folder /var/bmad-output +``` + +## Validation and Error Handling + +BMAD validates all provided flags: + +- **Directory**: Must be a valid path with write permissions +- **Modules**: Will warn about invalid module IDs (but won't fail) +- **Tools**: Will warn about invalid tool IDs (but won't fail) +- **Custom Content**: Each path must contain a valid `module.yaml` file +- **Action**: Must be one of: install, update, quick-update, compile-agents + +Invalid values will either: +1. Show an error and exit (for critical options like directory) +2. Show a warning and skip (for optional items like custom content) +3. Fall back to interactive prompts (for missing required values) + +## Tips and Best Practices + +1. **Use absolute paths** for `--directory` to avoid ambiguity +2. **Test flags locally** before using in CI/CD pipelines +3. **Combine with `-y`** for truly unattended installations +4. **Check module availability** by running the interactive installer once +5. **Use `--debug`** flag if you encounter issues during installation +6. **Skip tool configuration** with `--tools none` for server/CI environments where IDEs aren't needed +7. **Partial flags are OK** - Omit flags and let BMAD prompt for missing values interactively + +## Troubleshooting + +### Installation fails with "Invalid directory" + +Check that: +- The directory path exists or its parent exists +- You have write permissions +- The path is absolute or correctly relative to current directory + +### Module not found + +- Verify the module ID is correct (check available modules in interactive mode) +- External modules may need to be available in the registry + +### Custom content path invalid + +Ensure each custom content path: +- Points to a directory +- Contains a `module.yaml` file in the root +- Has a `code` field in the `module.yaml` + +## Feedback and Issues + +If you encounter any issues with non-interactive installation: + +1. Run with `--debug` flag for detailed output +2. Try the interactive mode to verify the issue +3. Report issues on GitHub: diff --git a/docs/reference/agents.md b/docs/reference/agents.md new file mode 100644 index 000000000..87a0927c3 --- /dev/null +++ b/docs/reference/agents.md @@ -0,0 +1,23 @@ +--- +title: Agents +description: Default BMM agents with their menu triggers and primary workflows +--- + +This page lists the default BMM (Agile suite) agents that install with BMAD Method, along with their menu triggers and primary workflows. + +Notes: +- Triggers are the short menu codes (e.g., `CP`) and fuzzy matches shown in each agent menu. +- Slash commands are generated separately. See `docs/reference/commands.md` for the slash command list and where they are defined. +- QA (Quinn) is the lightweight test automation agent in BMM. The full Test Architect (TEA) lives in its own module. + +| Agent | Triggers | Primary workflows | +| --------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- | +| Analyst (Mary) | `BP`, `RS`, `CB`, `DP` | Brainstorm Project, Research, Create Brief, Document Project | +| Product Manager (John) | `CP`, `VP`, `EP`, `CE`, `IR`, `CC` | Create/Validate/Edit PRD, Create Epics and Stories, Implementation Readiness, Correct Course | +| Architect (Winston) | `CA`, `IR` | Create Architecture, Implementation Readiness | +| Scrum Master (Bob) | `SP`, `CS`, `ER`, `CC` | Sprint Planning, Create Story, Epic Retrospective, Correct Course | +| Developer (Amelia) | `DS`, `CR` | Dev Story, Code Review | +| QA Engineer (Quinn) | `QA` | Automate (generate tests for existing features) | +| Quick Flow Solo Dev (Barry) | `QS`, `QD`, `CR` | Quick Spec, Quick Dev, Code Review | +| UX Designer (Sally) | `CU` | Create UX Design | +| Technical Writer (Paige) | `DP`, `WD`, `US`, `MG`, `VD`, `EC` | Document Project, Write Document, Update Standards, Mermaid Generate, Validate Doc, Explain Concept | diff --git a/docs/reference/commands.md b/docs/reference/commands.md new file mode 100644 index 000000000..39b97f637 --- /dev/null +++ b/docs/reference/commands.md @@ -0,0 +1,34 @@ +--- +title: Commands +description: How BMAD commands are generated and where to find them. +--- + +# Commands + +BMAD slash commands are generated by the installer for your IDE and **reflect the modules you have installed**. +That means the authoritative list lives **in your project**, not in a static docs page. + +## How to Discover Commands (Recommended) + +- Type `/bmad` in your IDE and use autocomplete to browse agents/workflows. +- Run `/bmad-help` to get guided next steps and context-aware recommendations. + +## Where Commands Are Generated + +The installer writes command files into your project (example paths for Claude Code): + +- `.claude/commands/bmad//agents/` +- `.claude/commands/bmad//workflows/` + +These folders are the **canonical, project-specific command list**. + +## Common Commands + +- `/bmad-help` - Interactive help and next-step guidance +- `/bmad::agents:` - Load an agent (e.g. `/bmad:bmm:agents:dev`) +- `/bmad::workflows:` - Run a workflow (e.g. `/bmad:bmm:workflows:create-prd`) + +## Why This Page Is Short + +BMAD is modular, so the exact commands vary by install. +Use your IDE's autocomplete or the generated command folders above to see *everything* available. diff --git a/docs/reference/modules.md b/docs/reference/modules.md new file mode 100644 index 000000000..a0f6fdd6f --- /dev/null +++ b/docs/reference/modules.md @@ -0,0 +1,74 @@ +--- +title: Official Modules +description: Add-on modules for building custom agents, creative intelligence, game development, and testing +--- + +BMad extends through official modules that you select during installation. These add-on modules provide specialized agents, workflows, and tasks for specific domains beyond the built-in core and BMM (Agile suite). + +:::tip[Installing Modules] +Run `npx bmad-method install` and select the modules you want. The installer handles downloading, configuration, and IDE integration automatically. +::: + +## BMad Builder + +Create custom agents, workflows, and domain-specific modules with guided assistance. BMad Builder is the meta-module for extending the framework itself. + +- **Code:** `bmb` +- **npm:** [`bmad-builder`](https://www.npmjs.com/package/bmad-builder) +- **GitHub:** [bmad-code-org/bmad-builder](https://github.com/bmad-code-org/bmad-builder) + +**Provides:** + +- Agent Builder -- create specialized AI agents with custom expertise and tool access +- Workflow Builder -- design structured processes with steps and decision points +- Module Builder -- package agents and workflows into shareable, publishable modules +- Interactive setup with YAML configuration and npm publishing support + +## Creative Intelligence Suite + +AI-powered tools for structured creativity, ideation, and innovation during early-stage development. The suite provides multiple agents that facilitate brainstorming, design thinking, and problem-solving using proven frameworks. + +- **Code:** `cis` +- **npm:** [`bmad-creative-intelligence-suite`](https://www.npmjs.com/package/bmad-creative-intelligence-suite) +- **GitHub:** [bmad-code-org/bmad-module-creative-intelligence-suite](https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite) + +**Provides:** + +- Innovation Strategist, Design Thinking Coach, and Brainstorming Coach agents +- Problem Solver and Creative Problem Solver for systematic and lateral thinking +- Storyteller and Presentation Master for narratives and pitches +- Ideation frameworks including SCAMPER, Reverse Brainstorming, and problem reframing + +## Game Dev Studio + +Structured game development workflows adapted for Unity, Unreal, Godot, and custom engines. Supports rapid prototyping through Quick Flow and full-scale production with epic-driven sprints. + +- **Code:** `gds` +- **npm:** [`bmad-game-dev-studio`](https://www.npmjs.com/package/bmad-game-dev-studio) +- **GitHub:** [bmad-code-org/bmad-module-game-dev-studio](https://github.com/bmad-code-org/bmad-module-game-dev-studio) + +**Provides:** + +- Game Design Document (GDD) generation workflow +- Quick Dev mode for rapid prototyping +- Narrative design support for characters, dialogue, and world-building +- Coverage for 21+ game types with engine-specific architecture guidance + +## Test Architect (TEA) + +Enterprise-grade test strategy, automation guidance, and release gate decisions through an expert agent and nine structured workflows. TEA goes well beyond the built-in QA agent with risk-based prioritization and requirements traceability. + +- **Code:** `tea` +- **npm:** [`bmad-method-test-architecture-enterprise`](https://www.npmjs.com/package/bmad-method-test-architecture-enterprise) +- **GitHub:** [bmad-code-org/bmad-method-test-architecture-enterprise](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise) + +**Provides:** + +- Murat agent (Master Test Architect and Quality Advisor) +- Workflows for test design, ATDD, automation, test review, and traceability +- NFR assessment, CI setup, and framework scaffolding +- P0-P3 prioritization with optional Playwright Utils and MCP integrations + +## Community Modules + +Community modules and a module marketplace are coming. Check the [BMad GitHub organization](https://github.com/bmad-code-org) for updates. diff --git a/docs/reference/testing.md b/docs/reference/testing.md new file mode 100644 index 000000000..86af5294b --- /dev/null +++ b/docs/reference/testing.md @@ -0,0 +1,22 @@ +--- +title: Testing Options +description: Built-in QA agent and the standalone Test Architect module for advanced testing +--- + +# Testing Options + +BMad provides a built-in QA agent for quick test automation and a separate Test Architect (TEA) module for advanced testing. + +## Built-in QA (Quinn) + +Use the built-in QA agent for fast, straightforward test coverage: + +- Trigger: `QA` or `bmad-bmm-qa-automate` +- Best for: small projects, quick coverage, standard patterns + +## Test Architect (TEA) Module + +TEA is a standalone module with advanced testing workflows (test design, ATDD, automate, review, trace, NFR assessment). + +- Documentation: +- Install: `npx bmad-method@alpha install` and select the TEA module diff --git a/docs/reference/workflow-map.md b/docs/reference/workflow-map.md index 1425c4698..0df3d3ec8 100644 --- a/docs/reference/workflow-map.md +++ b/docs/reference/workflow-map.md @@ -73,7 +73,7 @@ Skip phases 1-3 for small, well-understood work. Each document becomes context for the next phase. The PRD tells the architect what constraints matter. The architecture tells the dev agent which patterns to follow. Story files give focused, complete context for implementation. Without this structure, agents make inconsistent decisions. -For brownfield projects, `document-project` creates or updates `project-context.md` - what exists in the codebase and the rules all implementation workflows must observe. Run it just before Phase 4, and again when something significant changes - structure, architecture, or those rules. You can also edit `project-context.md` by hand. +For established projects, `document-project` creates or updates `project-context.md` - what exists in the codebase and the rules all implementation workflows must observe. Run it just before Phase 4, and again when something significant changes - structure, architecture, or those rules. You can also edit `project-context.md` by hand. All implementation workflows load `project-context.md` if it exists. Additional context per workflow: diff --git a/eslint.config.mjs b/eslint.config.mjs index e361b1cdf..90dbf1553 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,11 +12,7 @@ export default [ 'coverage/**', '**/*.min.js', 'test/template-test-generator/**', - 'test/template-test-generator/**/*.js', - 'test/template-test-generator/**/*.md', 'test/fixtures/**', - 'test/fixtures/**/*.yaml', - '_bmad/**', '_bmad*/**', // Build output 'build/**', @@ -36,6 +32,10 @@ export default [ 'tools/template-test-generator/test-scenarios/**', 'src/modules/*/sub-modules/**', '.bundler-temp/**', + // Augment vendor config — not project code, naming conventions + // are dictated by Augment and can't be changed, so exclude + // the entire directory from linting + '.augment/**', ], }, diff --git a/package-lock.json b/package-lock.json index fe41085b3..9f0ce7e21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,29 +1,26 @@ { "name": "bmad-method", - "version": "6.0.0-Beta.5", + "version": "6.0.0-Beta.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bmad-method", - "version": "6.0.0-Beta.5", + "version": "6.0.0-Beta.7", "license": "MIT", "dependencies": { - "@clack/prompts": "^0.11.0", + "@clack/core": "^1.0.0", + "@clack/prompts": "^1.0.0", "@kayvan/markdown-tree-parser": "^1.6.1", - "boxen": "^5.1.2", "chalk": "^4.1.2", - "cli-table3": "^0.6.5", "commander": "^14.0.0", "csv-parse": "^6.1.0", - "figlet": "^1.8.0", "fs-extra": "^11.3.0", "glob": "^11.0.3", "ignore": "^7.0.5", "js-yaml": "^4.1.0", - "ora": "^5.4.1", + "picocolors": "^1.1.1", "semver": "^7.6.3", - "wrap-ansi": "^7.0.0", "xml2js": "^0.6.2", "yaml": "^2.7.0" }, @@ -35,7 +32,6 @@ "@astrojs/sitemap": "^3.6.0", "@astrojs/starlight": "^0.37.5", "@eslint/js": "^9.33.0", - "archiver": "^7.0.1", "astro": "^5.16.0", "c8": "^10.1.3", "eslint": "^9.33.0", @@ -756,9 +752,9 @@ } }, "node_modules/@clack/core": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", - "integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0.tgz", + "integrity": "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ==", "license": "MIT", "dependencies": { "picocolors": "^1.0.0", @@ -766,26 +762,16 @@ } }, "node_modules/@clack/prompts": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz", - "integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.0.tgz", + "integrity": "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A==", "license": "MIT", "dependencies": { - "@clack/core": "0.5.0", + "@clack/core": "1.0.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/@ctrl/tinycolor": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", @@ -3948,19 +3934,6 @@ "win32" ] }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -4005,6 +3978,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.1.0" @@ -4093,131 +4067,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/archiver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", - "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "archiver-utils": "^5.0.2", - "async": "^3.2.4", - "buffer-crc32": "^1.0.0", - "readable-stream": "^4.0.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^6.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", - "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^10.0.0", - "graceful-fs": "^4.2.0", - "is-stream": "^2.0.1", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/archiver-utils/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/archiver-utils/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/archiver-utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4962,21 +4811,6 @@ "node": ">= 0.4" } }, - "node_modules/b4a": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", - "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -5130,21 +4964,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, "node_modules/base-64": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", @@ -5152,26 +4971,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -5209,59 +5008,12 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, - "node_modules/boxen": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", - "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.0", - "camelcase": "^6.2.0", - "chalk": "^4.1.0", - "cli-boxes": "^2.2.1", - "string-width": "^4.2.2", - "type-fest": "^0.20.2", - "widest-line": "^3.1.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -5330,40 +5082,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5432,6 +5150,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5610,18 +5329,6 @@ "node": ">=0.8.0" } }, - "node_modules/cli-boxes": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -5638,33 +5345,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, "node_modules/cli-truncate": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", @@ -5737,15 +5417,6 @@ "node": ">=8" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -5862,23 +5533,6 @@ "dev": true, "license": "ISC" }, - "node_modules/compress-commons": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", - "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^6.0.0", - "is-stream": "^2.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5935,40 +5589,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", - "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", - "dev": true, - "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6170,18 +5790,6 @@ "node": ">=0.10.0" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", @@ -7088,16 +6696,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -7105,26 +6703,6 @@ "dev": true, "license": "MIT" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7210,13 +6788,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -7299,21 +6870,6 @@ } } }, - "node_modules/figlet": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz", - "integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==", - "license": "MIT", - "dependencies": { - "commander": "^14.0.0" - }, - "bin": { - "figlet": "bin/index.js" - }, - "engines": { - "node": ">= 17.0.0" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8171,26 +7727,6 @@ "@babel/runtime": "^7.23.2" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -8287,6 +7823,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -8471,15 +8008,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -8515,18 +8043,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-wsl": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", @@ -8543,13 +8059,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9626,52 +9135,6 @@ "node": ">= 8" } }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -9828,13 +9291,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.iteratee": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.iteratee/-/lodash.iteratee-4.7.0.tgz", @@ -9848,22 +9304,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -11310,6 +10750,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11621,6 +11062,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -11669,81 +11111,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-limit": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", @@ -12326,23 +11693,6 @@ "node": ">=6" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -12440,81 +11790,6 @@ "dev": true, "license": "MIT" }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "dev": true, - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/readable-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -13143,26 +12418,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/sax": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", @@ -13515,27 +12770,6 @@ "dev": true, "license": "MIT" }, - "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -13868,18 +13102,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -13982,16 +13204,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -14142,18 +13354,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -14603,6 +13803,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -14768,15 +13969,6 @@ "makeerror": "1.0.12" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -14813,18 +14005,6 @@ "node": ">=4" } }, - "node_modules/widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "license": "MIT", - "dependencies": { - "string-width": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -14839,6 +14019,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -14895,6 +14076,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14904,6 +14086,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15142,21 +14325,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zip-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", - "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "archiver-utils": "^5.0.0", - "compress-commons": "^6.0.2", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 8df5ea009..54d9646a0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "bmad-method", - "version": "6.0.0-Beta.5", + "version": "6.0.0-Beta.7", "description": "Breakthrough Method of Agile AI-driven Development", "keywords": [ "agile", @@ -26,7 +26,7 @@ "scripts": { "bmad:install": "node tools/cli/bmad-cli.js install", "bundle": "node tools/cli/bundlers/bundle-web.js all", - "docs:build": "node tools/build-docs.js", + "docs:build": "node tools/build-docs.mjs", "docs:dev": "astro dev --root website", "docs:fix-links": "node tools/fix-doc-links.js", "docs:preview": "astro preview --root website", @@ -41,10 +41,6 @@ "lint:md": "markdownlint-cli2 \"**/*.md\"", "prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0", "rebundle": "node tools/cli/bundlers/bundle-web.js rebundle", - "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", "test": "npm run test:schemas && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check", "test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas", "test:install": "node test/test-installation-components.js", @@ -69,21 +65,18 @@ ] }, "dependencies": { - "@clack/prompts": "^0.11.0", + "@clack/core": "^1.0.0", + "@clack/prompts": "^1.0.0", "@kayvan/markdown-tree-parser": "^1.6.1", - "boxen": "^5.1.2", "chalk": "^4.1.2", - "cli-table3": "^0.6.5", "commander": "^14.0.0", "csv-parse": "^6.1.0", - "figlet": "^1.8.0", "fs-extra": "^11.3.0", "glob": "^11.0.3", "ignore": "^7.0.5", "js-yaml": "^4.1.0", - "ora": "^5.4.1", + "picocolors": "^1.1.1", "semver": "^7.6.3", - "wrap-ansi": "^7.0.0", "xml2js": "^0.6.2", "yaml": "^2.7.0" }, @@ -91,7 +84,6 @@ "@astrojs/sitemap": "^3.6.0", "@astrojs/starlight": "^0.37.5", "@eslint/js": "^9.33.0", - "archiver": "^7.0.1", "astro": "^5.16.0", "c8": "^10.1.3", "eslint": "^9.33.0", diff --git a/src/bmm/agents/analyst.agent.yaml b/src/bmm/agents/analyst.agent.yaml index f63420a5b..c340f69c1 100644 --- a/src/bmm/agents/analyst.agent.yaml +++ b/src/bmm/agents/analyst.agent.yaml @@ -1,5 +1,3 @@ -# Business Analyst Agent Definition - agent: metadata: id: "_bmad/bmm/agents/analyst.md" @@ -23,9 +21,17 @@ agent: data: "{project-root}/_bmad/bmm/data/project-context-template.md" description: "[BP] Brainstorm Project: Expert Guided Facilitation through a single or multiple techniques with a final report" - - trigger: RS or fuzzy match on research - exec: "{project-root}/_bmad/bmm/workflows/1-analysis/research/workflow.md" - description: "[RS] Research: Choose from or specify market, domain, competitive analysis, or technical research" + - trigger: MR or fuzzy match on market-research + exec: "{project-root}/_bmad/bmm/workflows/1-analysis/research/workflow-market-research.md" + description: "[MR] Market Research: Market analysis, competitive landscape, customer needs and trends" + + - trigger: DR or fuzzy match on domain-research + exec: "{project-root}/_bmad/bmm/workflows/1-analysis/research/workflow-domain-research.md" + description: "[DR] Domain Research: Industry domain deep dive, subject matter expertise and terminology" + + - trigger: TR or fuzzy match on technical-research + exec: "{project-root}/_bmad/bmm/workflows/1-analysis/research/workflow-technical-research.md" + description: "[TR] Technical Research: Technical feasibility, architecture options and implementation approaches" - trigger: CB or fuzzy match on product-brief exec: "{project-root}/_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md" diff --git a/src/bmm/agents/dev.agent.yaml b/src/bmm/agents/dev.agent.yaml index 404a108c5..d88166eda 100644 --- a/src/bmm/agents/dev.agent.yaml +++ b/src/bmm/agents/dev.agent.yaml @@ -1,7 +1,6 @@ # Dev Implementation Agent Definition (v6) agent: - webskip: true metadata: id: "_bmad/bmm/agents/dev.md" name: Amelia diff --git a/src/bmm/agents/pm.agent.yaml b/src/bmm/agents/pm.agent.yaml index 1fa22545e..9ce0bf32f 100644 --- a/src/bmm/agents/pm.agent.yaml +++ b/src/bmm/agents/pm.agent.yaml @@ -1,6 +1,3 @@ -# Product Manager Agent Definition -# This file defines the PM agent for the BMAD BMM module - agent: metadata: id: "_bmad/bmm/agents/pm.md" @@ -22,15 +19,15 @@ agent: menu: - trigger: CP or fuzzy match on create-prd - exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md" + exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md" description: "[CP] Create PRD: Expert led facilitation to produce your Product Requirements Document" - trigger: VP or fuzzy match on validate-prd - exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md" + exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md" description: "[VP] Validate PRD: Validate a Product Requirements Document is comprehensive, lean, well organized and cohesive" - trigger: EP or fuzzy match on edit-prd - exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md" + exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md" description: "[EP] Edit PRD: Update an existing Product Requirements Document" - trigger: CE or fuzzy match on epics-stories diff --git a/src/bmm/agents/qa.agent.yaml b/src/bmm/agents/qa.agent.yaml index bad945cd4..07ca4022f 100644 --- a/src/bmm/agents/qa.agent.yaml +++ b/src/bmm/agents/qa.agent.yaml @@ -27,7 +27,7 @@ agent: - Focus on realistic user scenarios menu: - - trigger: qa + - trigger: QA or fuzzy match on qa-automate workflow: "{project-root}/_bmad/bmm/workflows/qa/automate/workflow.yaml" description: "[QA] Automate - Generate tests for existing features (simplified)" diff --git a/src/bmm/module-help.csv b/src/bmm/module-help.csv index 45eeb6ab3..635bb8a81 100644 --- a/src/bmm/module-help.csv +++ b/src/bmm/module-help.csv @@ -4,29 +4,22 @@ bmm,anytime,Generate Project Context,GPC,,_bmad/bmm/workflows/generate-project-c bmm,anytime,Quick Spec,QS,,_bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md,bmad-bmm-quick-spec,false,quick-flow-solo-dev,Create Mode,"Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method. Quick one-off tasks small changes simple apps brownfield additions to well established patterns utilities without extensive planning",planning_artifacts,"tech spec", bmm,anytime,Quick Dev,QD,,_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md,bmad-bmm-quick-dev,false,quick-flow-solo-dev,Create Mode,"Quick one-off tasks small changes simple apps utilities without extensive planning - Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method, unless the user is already working through the implementation phase and just requests a 1 off things not already in the plan",,, bmm,anytime,Correct Course,CC,,_bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml,bmad-bmm-correct-course,false,sm,Create Mode,"Anytime: Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories",planning_artifacts,"change proposal", -bmm,anytime,Create Dataflow,CDF,,_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml,bmad-bmm-create-excalidraw-dataflow,false,ux-designer,Create Mode,"Create data flow diagrams (DFD) in Excalidraw format - can be called standalone or during any workflow to add visual documentation",planning_artifacts,"dataflow diagram", -bmm,anytime,Create Diagram,CED,,_bmad/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml,bmad-bmm-create-excalidraw-diagram,false,ux-designer,Create Mode,"Create system architecture diagrams ERDs UML diagrams or general technical diagrams in Excalidraw format - use anytime or call from architecture workflow to add visual documentation",planning_artifacts,"diagram", -bmm,anytime,Create Flowchart,CFC,,_bmad/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml,bmad-bmm-create-excalidraw-flowchart,false,ux-designer,Create Mode,"Create a flowchart visualization in Excalidraw format for processes pipelines or logic flows - use anytime or during architecture to add process documentation",planning_artifacts,"flowchart", -bmm,anytime,Create Wireframe,CEW,,_bmad/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml,bmad-bmm-create-excalidraw-wireframe,false,ux-designer,Create Mode,"Create website or app wireframes in Excalidraw format - use anytime standalone or call from UX workflow to add UI mockups",planning_artifacts,"wireframe", -bmm,anytime,Write Document,WD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,bmad-bmm-write-document,false,tech-writer,,"Describe in detail what you want, and the agent will follow the documentation best practices defined in agent memory. Multi-turn conversation with subprocess for research/review.",project-knowledge,"document", -bmm,anytime,Update Standards,US,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,bmad-bmm-update-standards,false,tech-writer,,"Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.",_bmad/_memory/tech-writer-sidecar,"standards", -bmm,anytime,Mermaid Generate,MG,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,bmad-bmm-mermaid-generate,false,tech-writer,,"Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.",planning_artifacts,"mermaid diagram", -bmm,anytime,Validate Document,VD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,bmad-bmm-validate-document,false,tech-writer,,"Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.",planning_artifacts,"validation report", -bmm,anytime,Explain Concept,EC,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,bmad-bmm-explain-concept,false,tech-writer,,"Create clear technical explanations with examples and diagrams for complex concepts. Breaks down into digestible sections using task-oriented approach.",project_knowledge,"explanation", +bmm,anytime,Write Document,WD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Describe in detail what you want, and the agent will follow the documentation best practices defined in agent memory. Multi-turn conversation with subprocess for research/review.",project-knowledge,"document", +bmm,anytime,Update Standards,US,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.",_bmad/_memory/tech-writer-sidecar,"standards", +bmm,anytime,Mermaid Generate,MG,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.",planning_artifacts,"mermaid diagram", +bmm,anytime,Validate Document,VD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.",planning_artifacts,"validation report", +bmm,anytime,Explain Concept,EC,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Create clear technical explanations with examples and diagrams for complex concepts. Breaks down into digestible sections using task-oriented approach.",project_knowledge,"explanation", bmm,1-analysis,Brainstorm Project,BP,10,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,data=_bmad/bmm/data/project-context-template.md,"Expert Guided Facilitation through a single or multiple techniques",planning_artifacts,"brainstorming session", -bmm,1-analysis,Market Research,MR,20,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=market,"Market analysis competitive landscape customer needs and trends","planning_artifacts|project-knowledge","research documents", -bmm,1-analysis,Domain Research,DR,21,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=domain,"Industry domain deep dive subject matter expertise and terminology","planning_artifacts|project_knowledge","research documents", -bmm,1-analysis,Technical Research,TR,22,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=technical,"Technical feasibility architecture options and implementation approaches","planning_artifacts|project_knowledge","research documents", -bmm,1-analysis,Create Brief,CB,30,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-create-brief,false,analyst,Create Mode,"A guided experience to nail down your product idea",planning_artifacts,"product brief", -bmm,1-analysis,Validate Brief,VB,40,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-validate-brief,false,analyst,Validate Mode,"Validates product brief completeness",planning_artifacts,"brief validation report", -bmm,2-planning,Create PRD,CP,10,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad-bmm-create-prd,true,pm,Create Mode,"Expert led facilitation to produce your Product Requirements Document",planning_artifacts,prd, -bmm,2-planning,Validate PRD,VP,20,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad-bmm-validate-prd,false,pm,Validate Mode,"Validate PRD is comprehensive lean well organized and cohesive",planning_artifacts,"prd validation report", +bmm,1-analysis,Market Research,MR,20,_bmad/bmm/workflows/1-analysis/research/workflow-market-research.md,bmad-bmm-market-research,false,analyst,Create Mode,"Market analysis competitive landscape customer needs and trends","planning_artifacts|project-knowledge","research documents", +bmm,1-analysis,Domain Research,DR,21,_bmad/bmm/workflows/1-analysis/research/workflow-domain-research.md,bmad-bmm-domain-research,false,analyst,Create Mode,"Industry domain deep dive subject matter expertise and terminology","planning_artifacts|project_knowledge","research documents", +bmm,1-analysis,Technical Research,TR,22,_bmad/bmm/workflows/1-analysis/research/workflow-technical-research.md,bmad-bmm-technical-research,false,analyst,Create Mode,"Technical feasibility architecture options and implementation approaches","planning_artifacts|project_knowledge","research documents", +bmm,1-analysis,Create Brief,CB,30,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-create-product-brief,false,analyst,Create Mode,"A guided experience to nail down your product idea",planning_artifacts,"product brief", +bmm,2-planning,Create PRD,CP,10,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md,bmad-bmm-create-prd,true,pm,Create Mode,"Expert led facilitation to produce your Product Requirements Document",planning_artifacts,prd, +bmm,2-planning,Validate PRD,VP,20,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md,bmad-bmm-validate-prd,false,pm,Validate Mode,"Validate PRD is comprehensive lean well organized and cohesive",planning_artifacts,"prd validation report", +bmm,2-planning,Edit PRD,EP,25,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md,bmad-bmm-edit-prd,false,pm,Edit Mode,"Improve and enhance an existing PRD",planning_artifacts,"updated prd", bmm,2-planning,Create UX,CU,30,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad-bmm-create-ux-design,false,ux-designer,Create Mode,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project",planning_artifacts,"ux design", -bmm,2-planning,Validate UX,VU,40,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad-bmm-create-ux-design,false,ux-designer,Validate Mode,"Validates UX design deliverables",planning_artifacts,"ux validation report", bmm,3-solutioning,Create Architecture,CA,10,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad-bmm-create-architecture,true,architect,Create Mode,"Guided Workflow to document technical decisions",planning_artifacts,architecture, -bmm,3-solutioning,Validate Architecture,VA,20,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad-bmm-create-architecture,false,architect,Validate Mode,"Validates architecture completeness",planning_artifacts,"architecture validation report", bmm,3-solutioning,Create Epics and Stories,CE,30,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad-bmm-create-epics-and-stories,true,pm,Create Mode,"Create the Epics and Stories Listing",planning_artifacts,"epics and stories", -bmm,3-solutioning,Validate Epics and Stories,VE,40,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad-bmm-create-epics-and-stories,false,pm,Validate Mode,"Validates epics and stories completeness",planning_artifacts,"epics validation report", bmm,3-solutioning,Check Implementation Readiness,IR,70,_bmad/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md,bmad-bmm-check-implementation-readiness,true,architect,Validate Mode,"Ensure PRD UX Architecture and Epics Stories are aligned",planning_artifacts,"readiness report", bmm,4-implementation,Sprint Planning,SP,10,_bmad/bmm/workflows/4-implementation/sprint-planning/workflow.yaml,bmad-bmm-sprint-planning,true,sm,Create Mode,"Generate sprint plan for development tasks - this kicks off the implementation phase by producing a plan the implementation agents will follow in sequence for every story in the plan.",implementation_artifacts,"sprint status", bmm,4-implementation,Sprint Status,SS,20,_bmad/bmm/workflows/4-implementation/sprint-status/workflow.yaml,bmad-bmm-sprint-status,false,sm,Create Mode,"Anytime: Summarize sprint status and route to next workflow",,, diff --git a/src/bmm/workflows/1-analysis/create-product-brief/steps/step-06-complete.md b/src/bmm/workflows/1-analysis/create-product-brief/steps/step-06-complete.md index 91c1ba66e..010cafe8e 100644 --- a/src/bmm/workflows/1-analysis/create-product-brief/steps/step-06-complete.md +++ b/src/bmm/workflows/1-analysis/create-product-brief/steps/step-06-complete.md @@ -128,7 +128,7 @@ Recap that the brief captures everything needed to guide subsequent product deve ### 5. Suggest next steps -Product Brief complete. Read fully and follow: `_bmad/core/tasks/bmad-help.md` with argument `Validate PRD`. +Product Brief complete. Read fully and follow: `_bmad/core/tasks/help.md` with argument `Validate PRD`. --- diff --git a/src/bmm/workflows/1-analysis/create-product-brief/workflow.md b/src/bmm/workflows/1-analysis/create-product-brief/workflow.md index c17b18215..9d5e83f19 100644 --- a/src/bmm/workflows/1-analysis/create-product-brief/workflow.md +++ b/src/bmm/workflows/1-analysis/create-product-brief/workflow.md @@ -1,7 +1,6 @@ --- name: create-product-brief description: Create comprehensive product briefs through collaborative step-by-step discovery as creative Business Analyst working with the user as peers. -web_bundle: true --- # Product Brief Workflow diff --git a/src/bmm/workflows/1-analysis/research/technical-steps/step-04-architectural-patterns.md b/src/bmm/workflows/1-analysis/research/technical-steps/step-04-architectural-patterns.md index 426cc6623..3d0e66ab3 100644 --- a/src/bmm/workflows/1-analysis/research/technical-steps/step-04-architectural-patterns.md +++ b/src/bmm/workflows/1-analysis/research/technical-steps/step-04-architectural-patterns.md @@ -155,7 +155,7 @@ Show the generated architectural patterns and present continue option: #### If 'C' (Continue): - Append the final content to the research document -- Update frontmatter: `stepsCompleted: [1, 2, 3]` +- Update frontmatter: `stepsCompleted: [1, 2, 3, 4]` - Load: `./step-05-implementation-research.md` ## APPEND TO DOCUMENT: diff --git a/src/bmm/workflows/1-analysis/research/technical-steps/step-05-implementation-research.md b/src/bmm/workflows/1-analysis/research/technical-steps/step-05-implementation-research.md index 7117d5251..994537356 100644 --- a/src/bmm/workflows/1-analysis/research/technical-steps/step-05-implementation-research.md +++ b/src/bmm/workflows/1-analysis/research/technical-steps/step-05-implementation-research.md @@ -1,4 +1,4 @@ -# Technical Research Step 4: Implementation Research +# Technical Research Step 5: Implementation Research ## MANDATORY EXECUTION RULES (READ FIRST): @@ -17,7 +17,7 @@ - 🎯 Show web search analysis before presenting findings - ⚠️ Present [C] complete option after implementation research content generation - 💾 ONLY save when user chooses C (Complete) -- 📖 Update frontmatter `stepsCompleted: [1, 2, 3, 4]` before completing workflow +- 📖 Update frontmatter `stepsCompleted: [1, 2, 3, 4, 5]` before completing workflow - 🚫 FORBIDDEN to complete workflow until C is selected ## CONTEXT BOUNDARIES: @@ -25,7 +25,7 @@ - Current document and frontmatter from previous steps are available - Focus on implementation approaches and technology adoption strategies - Web search capabilities with source verification are enabled -- This is the final step in the technical research workflow +- This step prepares for the final synthesis step ## YOUR TASK: @@ -149,10 +149,10 @@ _Source: [URL]_ [Success measurement framework] ``` -### 6. Present Analysis and Complete Option +### 6. Present Analysis and Continue Option -Show the generated implementation research and present complete option: -"I've completed the **implementation research and technology adoption** analysis, finalizing our comprehensive technical research. +Show the generated implementation research and present continue option: +"I've completed the **implementation research and technology adoption** analysis for {{research_topic}}. **Implementation Highlights:** @@ -162,23 +162,24 @@ Show the generated implementation research and present complete option: - Team organization and skill requirements identified - Cost optimization and resource management strategies provided -**This completes our technical research covering:** +**Technical research phases completed:** -- Technical overview and landscape analysis -- Architectural patterns and design decisions -- Implementation approaches and technology adoption -- Practical recommendations and implementation roadmap +- Step 1: Research scope confirmation +- Step 2: Technology stack analysis +- Step 3: Integration patterns analysis +- Step 4: Architectural patterns analysis +- Step 5: Implementation research (current step) -**Ready to complete the technical research report?** -[C] Complete Research - Save final document and conclude +**Ready to proceed to the final synthesis step?** +[C] Continue - Save this to document and proceed to synthesis -### 7. Handle Complete Selection +### 7. Handle Continue Selection -#### If 'C' (Complete Research): +#### If 'C' (Continue): - Append the final content to the research document -- Update frontmatter: `stepsCompleted: [1, 2, 3, 4]` -- Complete the technical research workflow +- Update frontmatter: `stepsCompleted: [1, 2, 3, 4, 5]` +- Load: `./step-06-research-synthesis.md` ## APPEND TO DOCUMENT: @@ -191,9 +192,9 @@ When user selects 'C', append the content directly to the research document usin ✅ Testing and deployment practices clearly documented ✅ Team organization and skill requirements mapped ✅ Cost optimization and risk mitigation strategies provided -✅ [C] complete option presented and handled correctly +✅ [C] continue option presented and handled correctly ✅ Content properly appended to document when C selected -✅ Technical research workflow completed successfully +✅ Proper routing to synthesis step (step-06) ## FAILURE MODES: @@ -202,8 +203,9 @@ When user selects 'C', append the content directly to the research document usin ❌ Missing critical technology adoption strategies ❌ Not providing practical implementation guidance ❌ Incomplete development workflows or operational practices analysis -❌ Not presenting completion option for research workflow +❌ Not presenting continue option to synthesis step ❌ Appending content without user selecting 'C' +❌ Not routing to step-06-research-synthesis.md ❌ **CRITICAL**: Reading only partial step file - leads to incomplete understanding and poor decisions ❌ **CRITICAL**: Proceeding with 'C' without fully reading and understanding the next step file @@ -221,19 +223,11 @@ When user selects 'C', append the content directly to the research document usin When 'C' is selected: -- All technical research steps completed -- Comprehensive technical research document generated -- All sections appended with source citations -- Technical research workflow status updated -- Final implementation recommendations provided to user +- Implementation research step completed +- Content appended to research document with source citations +- Frontmatter updated with stepsCompleted: [1, 2, 3, 4, 5] +- Ready to proceed to final synthesis step -## NEXT STEPS: +## NEXT STEP: -Technical research workflow complete. User may: - -- Use technical research to inform architecture decisions -- Conduct additional research on specific technologies -- Combine technical research with other research types for comprehensive insights -- Move forward with implementation based on technical insights - -Congratulations on completing comprehensive technical research! 🎉 +After user selects 'C', load `./step-06-research-synthesis.md` to produce the comprehensive technical research document with narrative introduction, detailed TOC, and executive summary. diff --git a/src/bmm/workflows/1-analysis/research/technical-steps/step-06-research-synthesis.md b/src/bmm/workflows/1-analysis/research/technical-steps/step-06-research-synthesis.md index 7dc28a2d9..27331f667 100644 --- a/src/bmm/workflows/1-analysis/research/technical-steps/step-06-research-synthesis.md +++ b/src/bmm/workflows/1-analysis/research/technical-steps/step-06-research-synthesis.md @@ -1,4 +1,4 @@ -# Technical Research Step 5: Technical Synthesis and Completion +# Technical Research Step 6: Technical Synthesis and Completion ## MANDATORY EXECUTION RULES (READ FIRST): @@ -18,7 +18,7 @@ - 🎯 Show web search analysis before presenting findings - ⚠️ Present [C] complete option after synthesis content generation - 💾 ONLY save when user chooses C (Complete) -- 📖 Update frontmatter `stepsCompleted: [1, 2, 3, 4, 5]` before completing workflow +- 📖 Update frontmatter `stepsCompleted: [1, 2, 3, 4, 5, 6]` before completing workflow - 🚫 FORBIDDEN to complete workflow until C is selected - 📚 GENERATE COMPLETE DOCUMENT STRUCTURE with intro, TOC, and summary @@ -417,7 +417,7 @@ _This comprehensive technical research document serves as an authoritative techn #### If 'C' (Complete Research): - Append the complete technical document to the research file -- Update frontmatter: `stepsCompleted: [1, 2, 3, 4, 5]` +- Update frontmatter: `stepsCompleted: [1, 2, 3, 4, 5, 6]` - Complete the technical research workflow - Provide final technical document delivery confirmation diff --git a/src/bmm/workflows/1-analysis/research/workflow-domain-research.md b/src/bmm/workflows/1-analysis/research/workflow-domain-research.md new file mode 100644 index 000000000..91fcbaa9a --- /dev/null +++ b/src/bmm/workflows/1-analysis/research/workflow-domain-research.md @@ -0,0 +1,54 @@ +--- +name: domain-research +description: Conduct domain research covering industry analysis, regulations, technology trends, and ecosystem dynamics using current web data and verified sources. +--- + +# Domain Research Workflow + +**Goal:** Conduct comprehensive domain/industry research using current web data and verified sources to produce complete research documents with compelling narratives and proper citations. + +**Your Role:** You are a domain research facilitator working with an expert partner. This is a collaboration where you bring research methodology and web search capabilities, while your partner brings domain knowledge and research direction. + +## PREREQUISITE + +**⛔ Web search required.** If unavailable, abort and tell the user. + +## CONFIGURATION + +Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve: +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as a system-generated value + +## QUICK TOPIC DISCOVERY + +"Welcome {{user_name}}! Let's get started with your **domain/industry research**. + +**What domain, industry, or sector do you want to research?** + +For example: +- 'The healthcare technology industry' +- 'Sustainable packaging regulations in Europe' +- 'Construction and building materials sector' +- 'Or any other domain you have in mind...'" + +### Topic Clarification + +Based on the user's topic, briefly clarify: +1. **Core Domain**: "What specific aspect of [domain] are you most interested in?" +2. **Research Goals**: "What do you hope to achieve with this research?" +3. **Scope**: "Should we focus broadly or dive deep into specific aspects?" + +## ROUTE TO DOMAIN RESEARCH STEPS + +After gathering the topic and goals: + +1. Set `research_type = "domain"` +2. Set `research_topic = [discovered topic from discussion]` +3. Set `research_goals = [discovered goals from discussion]` +4. Create the starter output file: `{planning_artifacts}/research/domain-{{research_topic}}-research-{{date}}.md` with exact copy of the `./research.template.md` contents +5. Load: `./domain-steps/step-01-init.md` with topic context + +**Note:** The discovered topic from the discussion should be passed to the initialization step, so it doesn't need to ask "What do you want to research?" again - it can focus on refining the scope for domain research. + +**✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`** diff --git a/src/bmm/workflows/1-analysis/research/workflow-market-research.md b/src/bmm/workflows/1-analysis/research/workflow-market-research.md new file mode 100644 index 000000000..5669e6f24 --- /dev/null +++ b/src/bmm/workflows/1-analysis/research/workflow-market-research.md @@ -0,0 +1,54 @@ +--- +name: market-research +description: Conduct market research covering market size, growth, competition, and customer insights using current web data and verified sources. +--- + +# Market Research Workflow + +**Goal:** Conduct comprehensive market research using current web data and verified sources to produce complete research documents with compelling narratives and proper citations. + +**Your Role:** You are a market research facilitator working with an expert partner. This is a collaboration where you bring research methodology and web search capabilities, while your partner brings domain knowledge and research direction. + +## PREREQUISITE + +**⛔ Web search required.** If unavailable, abort and tell the user. + +## CONFIGURATION + +Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve: +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as a system-generated value + +## QUICK TOPIC DISCOVERY + +"Welcome {{user_name}}! Let's get started with your **market research**. + +**What topic, problem, or area do you want to research?** + +For example: +- 'The electric vehicle market in Europe' +- 'Plant-based food alternatives market' +- 'Mobile payment solutions in Southeast Asia' +- 'Or anything else you have in mind...'" + +### Topic Clarification + +Based on the user's topic, briefly clarify: +1. **Core Topic**: "What exactly about [topic] are you most interested in?" +2. **Research Goals**: "What do you hope to achieve with this research?" +3. **Scope**: "Should we focus broadly or dive deep into specific aspects?" + +## ROUTE TO MARKET RESEARCH STEPS + +After gathering the topic and goals: + +1. Set `research_type = "market"` +2. Set `research_topic = [discovered topic from discussion]` +3. Set `research_goals = [discovered goals from discussion]` +4. Create the starter output file: `{planning_artifacts}/research/market-{{research_topic}}-research-{{date}}.md` with exact copy of the `./research.template.md` contents +5. Load: `./market-steps/step-01-init.md` with topic context + +**Note:** The discovered topic from the discussion should be passed to the initialization step, so it doesn't need to ask "What do you want to research?" again - it can focus on refining the scope for market research. + +**✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`** diff --git a/src/bmm/workflows/1-analysis/research/workflow-technical-research.md b/src/bmm/workflows/1-analysis/research/workflow-technical-research.md new file mode 100644 index 000000000..2ac5420ce --- /dev/null +++ b/src/bmm/workflows/1-analysis/research/workflow-technical-research.md @@ -0,0 +1,54 @@ +--- +name: technical-research +description: Conduct technical research covering technology evaluation, architecture decisions, and implementation approaches using current web data and verified sources. +--- + +# Technical Research Workflow + +**Goal:** Conduct comprehensive technical research using current web data and verified sources to produce complete research documents with compelling narratives and proper citations. + +**Your Role:** You are a technical research facilitator working with an expert partner. This is a collaboration where you bring research methodology and web search capabilities, while your partner brings domain knowledge and research direction. + +## PREREQUISITE + +**⛔ Web search required.** If unavailable, abort and tell the user. + +## CONFIGURATION + +Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve: +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as a system-generated value + +## QUICK TOPIC DISCOVERY + +"Welcome {{user_name}}! Let's get started with your **technical research**. + +**What technology, tool, or technical area do you want to research?** + +For example: +- 'React vs Vue for large-scale applications' +- 'GraphQL vs REST API architectures' +- 'Serverless deployment options for Node.js' +- 'Or any other technical topic you have in mind...'" + +### Topic Clarification + +Based on the user's topic, briefly clarify: +1. **Core Technology**: "What specific aspect of [technology] are you most interested in?" +2. **Research Goals**: "What do you hope to achieve with this research?" +3. **Scope**: "Should we focus broadly or dive deep into specific aspects?" + +## ROUTE TO TECHNICAL RESEARCH STEPS + +After gathering the topic and goals: + +1. Set `research_type = "technical"` +2. Set `research_topic = [discovered topic from discussion]` +3. Set `research_goals = [discovered goals from discussion]` +4. Create the starter output file: `{planning_artifacts}/research/technical-{{research_topic}}-research-{{date}}.md` with exact copy of the `./research.template.md` contents +5. Load: `./technical-steps/step-01-init.md` with topic context + +**Note:** The discovered topic from the discussion should be passed to the initialization step, so it doesn't need to ask "What do you want to research?" again - it can focus on refining the scope for technical research. + +**✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`** diff --git a/src/bmm/workflows/1-analysis/research/workflow.md b/src/bmm/workflows/1-analysis/research/workflow.md deleted file mode 100644 index 64f62bef1..000000000 --- a/src/bmm/workflows/1-analysis/research/workflow.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -name: research -description: Conduct comprehensive research across multiple domains using current web data and verified sources - Market, Technical, Domain and other research types. -web_bundle: true ---- - -# Research Workflow - -**Goal:** Conduct comprehensive, exhaustive research across multiple domains using current web data and verified sources to produce complete research documents with compelling narratives and proper citations. - -**Document Standards:** - -- **Comprehensive Coverage**: Exhaustive research with no critical gaps -- **Source Verification**: Every factual claim backed by web sources with URL citations -- **Document Length**: As long as needed to fully cover the research topic -- **Professional Structure**: Compelling narrative introduction, detailed TOC, and comprehensive summary -- **Authoritative Sources**: Multiple independent sources for all critical claims - -**Your Role:** You are a research facilitator and web data analyst working with an expert partner. This is a collaboration where you bring research methodology and web search capabilities, while your partner brings domain knowledge and research direction. - -**Final Deliverable**: A complete research document that serves as an authoritative reference on the research topic with: - -- Compelling narrative introduction -- Comprehensive table of contents -- Detailed research sections with proper citations -- Executive summary and conclusions - -## WORKFLOW ARCHITECTURE - -This uses **micro-file architecture** with **routing-based discovery**: - -- Each research type has its own step folder -- Step 01 discovers research type and routes to appropriate sub-workflow -- Sequential progression within each research type -- Document state tracked in output frontmatter - -## INITIALIZATION - -### Configuration Loading - -Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve: - -- `project_name`, `output_folder`, , `planning_artifacts`, `user_name` -- `communication_language`, `document_output_language`, `user_skill_level` -- `date` as a system-generated value - -### Paths - -- `installed_path` = `{project-root}/_bmad/bmm/workflows/1-analysis/research` -- `template_path` = `{installed_path}/research.template.md` -- `default_output_file` = `{planning_artifacts}/research/{{research_type}}-{{topic}}-research-{{date}}.md` (dynamic based on research type) - -## PREREQUISITE - -**⛔ Web search required.** If unavailable, abort and tell the user. - -## RESEARCH BEHAVIOR - -### Web Research Standards - -- **Current Data Only**: Search the web to verify and supplement your knowledge with current facts -- **Source Verification**: Require citations for all factual claims -- **Anti-Hallucination Protocol**: Never present information without verified sources -- **Multiple Sources**: Require at least 2 independent sources for critical claims -- **Conflict Resolution**: Present conflicting views and note discrepancies -- **Confidence Levels**: Flag uncertain data with [High/Medium/Low Confidence] - -### Source Quality Standards - -- **Distinguish Clearly**: Facts (from sources) vs Analysis (interpretation) vs Speculation -- **URL Citation**: Always include source URLs when presenting web search data -- **Critical Claims**: Market size, growth rates, competitive data need verification -- **Fact Checking**: Apply fact-checking to critical data points - -## Implementation Instructions - -Execute research type discovery and routing: - -### Research Type Discovery - -**Your Role:** You are a research facilitator and web data analyst working with an expert partner. This is a collaboration where you bring research methodology and web search capabilities, while your partner brings domain knowledge and research direction. - -**Research Standards:** - -- **Anti-Hallucination Protocol**: Never present information without verified sources -- **Current Data Only**: Search the web to verify and supplement your knowledge with current facts -- **Source Citation**: Always include URLs for factual claims from web searches -- **Multiple Sources**: Require 2+ independent sources for critical claims -- **Conflict Resolution**: Present conflicting views and note discrepancies -- **Confidence Levels**: Flag uncertain data with [High/Medium/Low Confidence] - -### Collaborative Research Discovery - -"Welcome {{user_name}}! I'm excited to work with you as your research partner. I bring web research capabilities with rigorous source verification, while you bring the domain expertise and research direction. - -**Let me help you clarify what you'd like to research.** - -**First, tell me: What specific topic, problem, or area do you want to research?** - -For example: - -- 'The electric vehicle market in Europe' -- 'Cloud migration strategies for healthcare' -- 'AI implementation in financial services' -- 'Sustainable packaging regulations' -- 'Or anything else you have in mind...' - -### Topic Exploration and Clarification - -Based on the user's initial topic, explore and refine the research scope: - -#### Topic Clarification Questions: - -1. **Core Topic**: "What exactly about [topic] are you most interested in?" -2. **Research Goals**: "What do you hope to achieve with this research?" -3. **Scope**: "Should we focus broadly or dive deep into specific aspects?" -4. **Timeline**: "Are you looking at current state, historical context, or future trends?" -5. **Application**: "How will you use this research? (product development, strategy, academic, etc.)" - -#### Context Building: - -- **Initial Input**: User provides topic or research interest -- **Collaborative Refinement**: Work together to clarify scope and objectives -- **Goal Alignment**: Ensure research direction matches user needs -- **Research Boundaries**: Establish clear focus areas and deliverables - -### Research Type Identification - -After understanding the research topic and goals, identify the most appropriate research approach: - -**Research Type Options:** - -1. **Market Research** - Market size, growth, competition, customer insights - _Best for: Understanding market dynamics, customer behavior, competitive landscape_ - -2. **Domain Research** - Industry analysis, regulations, technology trends in specific domain - _Best for: Understanding industry context, regulatory environment, ecosystem_ - -3. **Technical Research** - Technology evaluation, architecture decisions, implementation approaches - _Best for: Technical feasibility, technology selection, implementation strategies_ - -**Recommendation**: Based on [topic] and [goals], I recommend [suggested research type] because [specific rationale]. - -**What type of research would work best for your needs?** - -### Research Type Routing - -Based on user selection, route to appropriate sub-workflow with the discovered topic using the following IF block sets of instructions. YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}` - -#### If Market Research: - -- Set `research_type = "market"` -- Set `research_topic = [discovered topic from discussion]` -- Create the starter output file: `{planning_artifacts}/research/market-{{research_topic}}-research-{{date}}.md` with exact copy of the ./research.template.md contents -- Load: `./market-steps/step-01-init.md` with topic context - -#### If Domain Research: - -- Set `research_type = "domain"` -- Set `research_topic = [discovered topic from discussion]` -- Create the starter output file: `{planning_artifacts}/research/domain-{{research_topic}}-research-{{date}}.md` with exact copy of the ./research.template.md contents -- Load: `./domain-steps/step-01-init.md` with topic context - -#### If Technical Research: - -- Set `research_type = "technical"` -- Set `research_topic = [discovered topic from discussion]` -- Create the starter output file: `{planning_artifacts}/research/technical-{{research_topic}}-research-{{date}}.md` with exact copy of the ./research.template.md contents -- Load: `./technical-steps/step-01-init.md` with topic context - -**Important**: The discovered topic from the collaborative discussion should be passed to the research initialization steps, so they don't need to ask "What do you want to research?" again - they can focus on refining the scope for their specific research type. - -**Note:** All research workflows require web search for current data and source verification. diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-12-complete.md b/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-12-complete.md index ec3272ff3..598d2c2ec 100644 --- a/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-12-complete.md +++ b/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-12-complete.md @@ -87,7 +87,7 @@ Offer validation workflows to ensure PRD is ready for implementation: ### 4. Suggest Next Workflows -PRD complete. Read fully and follow: `_bmad/core/tasks/bmad-help.md` with argument `Create PRD`. +PRD complete. Read fully and follow: `_bmad/core/tasks/help.md` with argument `Create PRD`. ### 5. Final Completion Confirmation diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-13-report-complete.md b/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-13-report-complete.md index 08465604b..15e69301a 100644 --- a/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-13-report-complete.md +++ b/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-13-report-complete.md @@ -197,7 +197,7 @@ Display: - **IF X (Exit):** - Display: "**Validation Report Saved:** {validationReportPath}" - Display: "**Summary:** {overall status} - {recommendation}" - - PRD Validation complete. Read fully and follow: `_bmad/core/tasks/bmad-help.md` with argument `Validate PRD`. + - PRD Validation complete. Read fully and follow: `_bmad/core/tasks/help.md` with argument `Validate PRD`. - **IF Any other:** Help user, then redisplay menu diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/validation-report-prd-workflow.md b/src/bmm/workflows/2-plan-workflows/create-prd/validation-report-prd-workflow.md deleted file mode 100644 index faa41ff64..000000000 --- a/src/bmm/workflows/2-plan-workflows/create-prd/validation-report-prd-workflow.md +++ /dev/null @@ -1,433 +0,0 @@ ---- -validationTarget: 'PRD Workflow Structure' -validationDate: '2026-01-08' -inputDocuments: [] -validationStepsCompleted: ['discovery', 'frontmatter-validation', 'content-validation', 'documentation-validation', 'integration-validation', 'corrections-applied'] -validationStatus: COMPLETE - PRODUCTION READY ---- - -# PRD Workflow Validation Report - -**Workflow Being Validated:** _bmad/bmm/workflows/2-plan-workflows/create-prd -**Validation Date:** 2026-01-08 -**Validator:** BMAD Workflow Validation System - ---- - -## Executive Summary - -This validation report assesses the PRD workflow structure against BMAD workflow standards. The PRD workflow is a tri-modal workflow system with Create, Validate, and Edit phases. - ---- - -## 1. File Structure & Size Analysis - -### Folder Structure - -``` -prd/ -├── workflow.md (main workflow file) -├── steps-c/ (Create steps - 12 files) -├── steps-v/ (Validation steps - 13 files) -├── steps-e/ (Edit steps - 5 files) -├── data/ -│ └── prd-purpose.md -└── templates/ - └── prd-template.md -``` - -**✅ Structure Status**: PASS - All required folders present - -### File Size Analysis - -#### Steps-C (Create Steps) - 12 files -| File | Lines | Status | -| ------------------------ | ----- | ------------------- | -| step-01-init.md | 191 | ⚠️ Approaching limit | -| step-01b-continue.md | 153 | ✅ Good | -| step-02-discovery.md | 197 | ⚠️ Approaching limit | -| step-03-success.md | 226 | ⚠️ Approaching limit | -| step-04-journeys.md | 213 | ⚠️ Approaching limit | -| step-05-domain.md | 193 | ⚠️ Approaching limit | -| step-06-innovation.md | 226 | ⚠️ Approaching limit | -| step-07-project-type.md | 225 | ⚠️ Approaching limit | -| step-08-scoping.md | 228 | ⚠️ Approaching limit | -| step-09-functional.md | 231 | ⚠️ Approaching limit | -| step-10-nonfunctional.md | 242 | ⚠️ Approaching limit | -| step-11-polish.md | 217 | ⚠️ Approaching limit | -| step-12-complete.md | 185 | ✅ Good | - -#### Steps-V (Validation Steps) - 13 files -| File | Lines | Status | -| ---------------------------------------------- | ----- | ------------------- | -| step-v-01-discovery.md | 217 | ⚠️ Approaching limit | -| step-v-02-format-detection.md | 191 | ⚠️ Approaching limit | -| step-v-02b-parity-check.md | 209 | ⚠️ Approaching limit | -| step-v-03-density-validation.md | 174 | ✅ Good | -| step-v-04-brief-coverage-validation.md | 214 | ⚠️ Approaching limit | -| step-v-05-measurability-validation.md | 228 | ⚠️ Approaching limit | -| step-v-06-traceability-validation.md | 217 | ⚠️ Approaching limit | -| step-v-07-implementation-leakage-validation.md | 205 | ⚠️ Approaching limit | -| step-v-08-domain-compliance-validation.md | 243 | ⚠️ Approaching limit | -| step-v-09-project-type-validation.md | 263 | ❌ Exceeds limit | -| step-v-10-smart-validation.md | 209 | ⚠️ Approaching limit | -| step-v-11-holistic-quality-validation.md | 264 | ❌ Exceeds limit | -| step-v-12-completeness-validation.md | 242 | ⚠️ Approaching limit | -| step-v-13-report-complete.md | 231 | ⚠️ Approaching limit | - -#### Steps-E (Edit Steps) - 5 files -| File | Lines | Status | -| ------------------------------- | ----- | ------------------- | -| step-e-01-discovery.md | 206 | ⚠️ Approaching limit | -| step-e-01b-legacy-conversion.md | 208 | ⚠️ Approaching limit | -| step-e-02-review.md | 249 | ⚠️ Approaching limit | -| step-e-03-edit.md | 253 | ❌ Exceeds limit | -| step-e-04-complete.md | 168 | ✅ Good | - -#### Data & Templates -| File | Lines | Status | -| ------------------------- | ----- | ------------------- | -| data/prd-purpose.md | 197 | ⚠️ Approaching limit | -| templates/prd-template.md | 10 | ✅ Good | -| workflow.md | 114 | ✅ Good | - -### File Size Statistics - -- **Total Files**: 32 markdown files -- **✅ Good (<200 lines)**: 6 files (18.8%) -- **⚠️ Approaching limit (200-250)**: 23 files (71.9%) -- **❌ Exceeds limit (>250)**: 3 files (9.4%) -- **Average lines per file**: 213.3 lines - -### ⚠️ Recommendations - -1. **Files Exceeding 250-line limit**: - - `step-v-09-project-type-validation.md` (263 lines) - Consider splitting into sub-steps - - `step-v-11-holistic-quality-validation.md` (264 lines) - Consider splitting into sub-steps - - `step-e-03-edit.md` (253 lines) - Consider splitting into sub-steps - -2. **Files Approaching Limit**: - - Many files are in the 200-250 line range - - Monitor these files as further additions may push them over the limit - - Consider proactive refactoring where appropriate - ---- - -## 2. Frontmatter Structure Validation - -### Files Checked: 29 total files - -**✅ Overall Status:** ALL VALID - One Issue Fixed - -#### Main Workflow (workflow.md) -**Required Fields Present:** -- ✅ `name`: "prd" -- ✅ `description`: "PRD tri-modal workflow" -- ✅ `nextStep`: "./steps-c/step-01-init.md" -- ✅ `validateWorkflow`: "./steps-v/step-v-01-discovery.md" -- ✅ `editWorkflow`: "./steps-e/step-e-01-discovery.md" (FIXED - was assess-workflow.md) - -#### Create Steps (steps-c) -- ✅ All 13 files have proper name, description, nextStepFile -- ✅ Proper sequencing from step-01 through step-12 -- ✅ Consistent output file references - -#### Validation Steps (steps-v) -- ✅ All 13 files have complete frontmatter -- ✅ Proper sequential chain maintained -- ✅ No broken internal references - -#### Edit Steps (steps-e) -- ✅ All files have required fields -- ✅ Proper routing with altStepFile references - -### ✅ All Issues Resolved - -**1. Broken Edit Workflow Reference:** -```yaml -# Current (INCORRECT): -editWorkflow: './steps-e/step-e-01-assess-workflow.md' - -# Should be: -editWorkflow: './steps-e/step-e-01-discovery.md' -``` - -**2. Step Numbering Gap:** -- Original `step-11-complete.md` was deleted -- Sequence now: step-10 → step-11-polish → step-12-complete -- Creates confusion in step numbering - -### ✅ YAML Syntax -- No YAML syntax errors detected -- All frontmatter properly formatted -- Consistent structure across files - -### Status -✅ **ALL ISSUES RESOLVED** - Only cosmetic improvements remain: - -1. **✅ FIXED**: Edit workflow path corrected in workflow.md -2. **⚠️ OPTIONAL**: Address step numbering gap for clarity -3. **⚠️ OPTIONAL**: Rename step-01b-continue.md to step-01a-continue.md for consistency - ---- - -## 3. Step File Content Validation - -### Content Quality Assessment: 4.5/5 - EXCELLENT - -#### Files Reviewed: 10 representative files across all modes - -#### ✅ Strengths - -**1. Comprehensive Structure:** -- Clear step goal sections in all files -- Detailed mandatory execution rules -- Well-defined execution protocols -- Context boundaries clearly specified -- Mandatory sequence with numbered steps -- System success/failure metrics present - -**2. BMAD Compliance:** -- ✅ JIT loading references consistently mentioned -- ✅ State tracking requirements documented -- ✅ Append-only building instructions present -- ✅ Critical rules properly emphasized with emojis -- ✅ Sequential enforcement clearly stated - -**3. Instructional Quality:** -- Clear, unambiguous instructions -- Proper menu handling rules (where applicable) -- Excellent continuation checks -- Strong role definition for each mode - -**4. Role Clarity:** -- Create Mode: "Product-focused PM facilitator" -- Validate Mode: "Validation Architect and Quality Assurance Specialist" -- Edit Mode: "PRD improvement specialist" - -#### ⚠️ Minor Improvement Opportunities - -**1. Header Formatting:** -- Some inconsistency in header level usage across files -- Recommend standardizing H2/H3 usage - -**2. Edit Mode Completeness:** -- Edit mode has fewer steps (5 vs 12/13 for other modes) -- Documentation marks it as "Future" but implementation exists - -#### Recommendations -1. **LOW PRIORITY**: Standardize header formatting across all step files -2. **LOW PRIORITY**: Complete remaining edit mode steps for parity -3. **MAINTAIN**: Current excellent quality standards - ---- - -## 4. Documentation Validation - -### Documentation Completeness: ✅ COMPREHENSIVE - -#### Main Components Present -- ✅ Workflow Definition (workflow.md) -- ✅ Purpose Document (data/prd-purpose.md) -- ✅ Template (templates/prd-template.md) -- ✅ Three Mode Implementations (Create: 12, Validate: 13, Edit: 5 steps) - -#### Clarity Assessment: ✅ EXCELLENT - -**Strong Points:** -1. Clear mode determination (commands, flags, menu selection) -2. Detailed routing instructions for each mode -3. Comprehensive workflow architecture explanation -4. Well-defined critical rules with visual emphasis -5. Professional presentation with consistent formatting - -#### ⚠️ Minor Issues Found - -**1. Step Count Mismatch:** -- workflow.md mentions "11 steps" for Create mode -- Actually implements 12 steps -- Could confuse users - -**2. Edit Mode Status:** -- workflow.md calls Edit mode "Future" -- Edit mode steps are actually implemented -- Should reflect current status - -**3. Template Completeness:** -- PRD template is minimal (10 lines) -- Could benefit from section placeholders - -**4. Missing README:** -- No onboarding documentation for new users -- Not critical but would be helpful - -#### Recommendations - -**HIGH PRIORITY:** -1. Fix step count reference to match implementation (12 steps) -2. Update edit mode documentation to "Implemented" - -**MEDIUM PRIORITY:** -3. Enhance PRD template with section structure -4. Add quick-start README for new users - -**LOW PRIORITY:** -5. Add troubleshooting section -6. Document external dependencies (domain-complexity.csv, project-types.csv) - ---- - -## 5. Integration & Compatibility Validation - -### Integration Status: 85% Ready - -#### ✅ Successfully Integrated Components - -**1. Agent Menu Registration:** -- ✅ Registered in PM agent menu -- ✅ Trigger: `PR` or fuzzy match on `prd` -- ✅ Command: `/bmad:bmm:workflows:create-prd` -- ✅ Proper workflow path configuration - -**2. External Workflow References:** -- ✅ Party-mode workflow: Exists at `{project-root}/_bmad/core/workflows/party-mode/workflow.md` -- ✅ Advanced-elicitation task: Exists at `{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml` - -**3. Directory Structure:** -- ✅ Complete step architecture (all 3 modes) -- ✅ All referenced step files exist -- ✅ Data files available - -#### ✅ Configuration & Installation - WORKING AS DESIGNED - -**1. BMM Config Reference:** -- Path: `{project-root}/_bmad/bmm/config.yaml` -- **Status:** ✅ Correct installation-time placeholder -- Resolves to actual config during workflow installation -- **Note:** This is expected behavior, not an issue - -**2. Planning Artifacts Folder:** -- Reference: `{planning_artifacts}/prd.md` -- **Status:** ✅ Correct installation-time placeholder -- Created/resolved during workflow installation -- **Note:** This is expected behavior, not an issue - -**3. Edit Mode Implementation:** -- Current: 5 steps (Discovery, Legacy Conversion branch, Review, Edit, Complete) -- **Status:** ✅ Functionally complete -- Edit mode is inherently simpler than create mode (targeted improvements vs full creation) -- Uses subprocesses for complex operations -- Validation integration ensures quality -- **Note:** Edit workflow is complete and well-designed - -#### Configuration Analysis - -**Placeholder Usage:** -- `{project-root}`: ✅ Properly used -- `{planning_artifacts}`: ⚠️ Referenced but folder missing -- `{nextStep}`, `{validateWorkflow}`, etc: ✅ Properly resolved - -#### Recommendations - -**✅ ALL CRITICAL ISSUES RESOLVED:** - -The only true critical issue (edit workflow path) has been fixed. All other items flagged as "critical" were actually working as designed (installation-time placeholders). - -**LOW PRIORITY:** -3. Add CLI command registration for standalone execution (optional enhancement) -4. Consider adding workflow to additional agent menus (UX designer, architect) -5. Create standalone execution documentation (nice-to-have) -6. Address step numbering gap if desired (cosmetic) - ---- - -## 6. Executive Summary & Overall Assessment - -### Overall Validation Status: ✅ PRODUCTION-READY - -#### Validation Scores by Category - -| Category | Status | Score | Notes | -| -------------------------- | ----------- | ------ | --------------------------------------------- | -| **File Structure & Size** | ⚠️ WARNINGS | 7/10 | 3 files exceed 250-line limit, 23 approaching | -| **Frontmatter Validation** | ✅ PASS | 9/10 | One broken path reference | -| **Step Content Quality** | ✅ EXCELLENT | 9.5/10 | High-quality instructional design | -| **Documentation** | ✅ EXCELLENT | 9/10 | Comprehensive, minor inconsistencies | -| **Integration** | ✅ PASS | 9/10 | All paths correct (one issue fixed) | -| **BMAD Compliance** | ✅ EXCELLENT | 9.5/10 | Strong adherence to standards | - -**Overall Score: 9.2/10 - EXCELLENT** - -#### ✅ Critical Action Items - ALL RESOLVED - -**ONLY ONE TRUE CRITICAL ISSUE EXISTED - NOW FIXED:** - -1. **✅ FIXED: Edit Workflow Path** - - File: `workflow.md` ✓ RESOLVED - - Changed from: `./steps-e/step-e-01-assess-workflow.md` - - Changed to: `./steps-e/step-e-01-discovery.md` - -**Items incorrectly flagged as critical (actually working as designed):** -- ✅ Configuration path references (installation-time placeholders) -- ✅ Planning artifacts folder (installation-time placeholder) - -#### High Priority Improvements - -2. **⚠️ Split Large Step Files** (>250 lines): - - `step-v-09-project-type-validation.md` (263 lines) - - `step-v-11-holistic-quality-validation.md` (264 lines) - - `step-e-03-edit.md` (253 lines) - -3. **⚠️ Update Documentation Inconsistencies**: - - Fix step count reference (11 → 12 steps in create mode) - - Update edit mode status (Future → Implemented) - -#### Medium Priority Enhancements - -4. **Enhance PRD Template** (currently minimal at 10 lines) -5. **Add quick-start README** for new users -6. **Address step numbering gap** (cosmetic - missing step-11-complete.md) - -#### Edit Mode Status - FUNCTIONALLY COMPLETE ✅ - -The edit workflow is **complete and well-designed** with 5 steps: -- Discovery → Legacy Conversion (branch) → Review → Edit → Complete -- Edit mode is inherently simpler than create mode (targeted improvements vs full creation) -- Uses subprocesses for complex operations -- Integrates with validation workflow - -**No additional steps needed.** - -### Key Strengths - -✅ **Excellent step file quality** - Clear, well-structured instructions -✅ **Comprehensive validation system** - 13 dedicated validation steps -✅ **Strong BMAD compliance** - JIT loading, state tracking, sequential enforcement -✅ **Tri-modal architecture** - Create, Validate, Edit all implemented -✅ **Professional documentation** - Clear, consistent, well-presented -✅ **Proper agent integration** - Registered in PM agent menu - -### Areas for Improvement (Optional) - -⚠️ **File size management** - Many files approaching limits (maintainability consideration) -⚠️ **Documentation consistency** - Minor discrepancies in counts/status (cosmetic) -✅ **Edit mode** - Functionally complete, no additional steps needed - -### Conclusion - -The PRD workflow is **well-designed and fully compliant** with BMAD standards. The step file architecture is exemplary, the content quality is excellent, and the documentation is comprehensive. The only critical issue (edit workflow path) has been **resolved**, and all other flagged items were actually working as designed (installation-time placeholders). - -**Current Status: ✅ PRODUCTION-READY** - -**Recommended Optional Enhancements:** -1. Split the 3 files exceeding 250-line limit (maintainability) -2. Update documentation inconsistencies (step counts, edit mode status) -3. Enhance PRD template and add quick-start README (user experience) - -The PRD workflow is ready for production use and fully compliant with BMAD workflow standards. - ---- - -**Validation Completed:** 2026-01-08 -**Validation Method:** Systematic subprocess analysis with maximum context coverage -**Validator:** BMAD Workflow Validation System (Wendy - Workflow Building Master) diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md new file mode 100644 index 000000000..7d10ec3ed --- /dev/null +++ b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md @@ -0,0 +1,63 @@ +--- +name: create-prd +description: Create a comprehensive PRD (Product Requirements Document) through structured workflow facilitation +main_config: '{project-root}/_bmad/bmm/config.yaml' +nextStep: './steps-c/step-01-init.md' +--- + +# PRD Create Workflow + +**Goal:** Create comprehensive PRDs through structured workflow facilitation. + +**Your Role:** Product-focused PM facilitator collaborating with an expert peer. + +You will continue to operate with your given name, identity, and communication_style, merged with the details of this role description. + +## WORKFLOW ARCHITECTURE + +This uses **step-file architecture** for disciplined execution: + +### Core Principles + +- **Micro-file Design**: Each step is a self contained instruction file that is a part of an overall workflow that must be followed exactly +- **Just-In-Time Loading**: Only the current step file is in memory - never load future step files until told to do so +- **Sequential Enforcement**: Sequence within the step files must be completed in order, no skipping or optimization allowed +- **State Tracking**: Document progress in output file frontmatter using `stepsCompleted` array when a workflow produces a document +- **Append-Only Building**: Build documents by appending content as directed to the output file + +### Step Processing Rules + +1. **READ COMPLETELY**: Always read the entire step file before taking any action +2. **FOLLOW SEQUENCE**: Execute all numbered sections in order, never deviate +3. **WAIT FOR INPUT**: If a menu is presented, halt and wait for user selection +4. **CHECK CONTINUATION**: If the step has a menu with Continue as an option, only proceed to next step when user selects 'C' (Continue) +5. **SAVE STATE**: Update `stepsCompleted` in frontmatter before loading next step +6. **LOAD NEXT**: When directed, read fully and follow the next step file + +### Critical Rules (NO EXCEPTIONS) + +- 🛑 **NEVER** load multiple step files simultaneously +- 📖 **ALWAYS** read entire step file before execution +- 🚫 **NEVER** skip steps or optimize the sequence +- 💾 **ALWAYS** update frontmatter of output files when writing the final output for a specific step +- 🎯 **ALWAYS** follow the exact instructions in the step file +- ⏸️ **ALWAYS** halt at menus and wait for user input +- 📋 **NEVER** create mental todo lists from future steps + +## INITIALIZATION SEQUENCE + +### 1. Configuration Loading + +Load and read full config from {main_config} and resolve: + +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as system-generated current datetime + +✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the configured `{communication_language}`. + +### 2. Route to Create Workflow + +"**Create Mode: Creating a new PRD from scratch.**" + +Read fully and follow: `{nextStep}` (steps-c/step-01-init.md) diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md new file mode 100644 index 000000000..5cb05af53 --- /dev/null +++ b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md @@ -0,0 +1,65 @@ +--- +name: edit-prd +description: Edit and improve an existing PRD - enhance clarity, completeness, and quality +main_config: '{project-root}/_bmad/bmm/config.yaml' +editWorkflow: './steps-e/step-e-01-discovery.md' +--- + +# PRD Edit Workflow + +**Goal:** Edit and improve existing PRDs through structured enhancement workflow. + +**Your Role:** PRD improvement specialist. + +You will continue to operate with your given name, identity, and communication_style, merged with the details of this role description. + +## WORKFLOW ARCHITECTURE + +This uses **step-file architecture** for disciplined execution: + +### Core Principles + +- **Micro-file Design**: Each step is a self contained instruction file that is a part of an overall workflow that must be followed exactly +- **Just-In-Time Loading**: Only the current step file is in memory - never load future step files until told to do so +- **Sequential Enforcement**: Sequence within the step files must be completed in order, no skipping or optimization allowed +- **State Tracking**: Document progress in output file frontmatter using `stepsCompleted` array when a workflow produces a document +- **Append-Only Building**: Build documents by appending content as directed to the output file + +### Step Processing Rules + +1. **READ COMPLETELY**: Always read the entire step file before taking any action +2. **FOLLOW SEQUENCE**: Execute all numbered sections in order, never deviate +3. **WAIT FOR INPUT**: If a menu is presented, halt and wait for user selection +4. **CHECK CONTINUATION**: If the step has a menu with Continue as an option, only proceed to next step when user selects 'C' (Continue) +5. **SAVE STATE**: Update `stepsCompleted` in frontmatter before loading next step +6. **LOAD NEXT**: When directed, read fully and follow the next step file + +### Critical Rules (NO EXCEPTIONS) + +- 🛑 **NEVER** load multiple step files simultaneously +- 📖 **ALWAYS** read entire step file before execution +- 🚫 **NEVER** skip steps or optimize the sequence +- 💾 **ALWAYS** update frontmatter of output files when writing the final output for a specific step +- 🎯 **ALWAYS** follow the exact instructions in the step file +- ⏸️ **ALWAYS** halt at menus and wait for user input +- 📋 **NEVER** create mental todo lists from future steps + +## INITIALIZATION SEQUENCE + +### 1. Configuration Loading + +Load and read full config from {main_config} and resolve: + +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as system-generated current datetime + +✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the configured `{communication_language}`. + +### 2. Route to Edit Workflow + +"**Edit Mode: Improving an existing PRD.**" + +Prompt for PRD path: "Which PRD would you like to edit? Please provide the path to the PRD.md file." + +Then read fully and follow: `{editWorkflow}` (steps-e/step-e-01-discovery.md) diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md new file mode 100644 index 000000000..67a1aafc8 --- /dev/null +++ b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md @@ -0,0 +1,65 @@ +--- +name: validate-prd +description: Validate an existing PRD against BMAD standards - comprehensive review for completeness, clarity, and quality +main_config: '{project-root}/_bmad/bmm/config.yaml' +validateWorkflow: './steps-v/step-v-01-discovery.md' +--- + +# PRD Validate Workflow + +**Goal:** Validate existing PRDs against BMAD standards through comprehensive review. + +**Your Role:** Validation Architect and Quality Assurance Specialist. + +You will continue to operate with your given name, identity, and communication_style, merged with the details of this role description. + +## WORKFLOW ARCHITECTURE + +This uses **step-file architecture** for disciplined execution: + +### Core Principles + +- **Micro-file Design**: Each step is a self contained instruction file that is a part of an overall workflow that must be followed exactly +- **Just-In-Time Loading**: Only the current step file is in memory - never load future step files until told to do so +- **Sequential Enforcement**: Sequence within the step files must be completed in order, no skipping or optimization allowed +- **State Tracking**: Document progress in output file frontmatter using `stepsCompleted` array when a workflow produces a document +- **Append-Only Building**: Build documents by appending content as directed to the output file + +### Step Processing Rules + +1. **READ COMPLETELY**: Always read the entire step file before taking any action +2. **FOLLOW SEQUENCE**: Execute all numbered sections in order, never deviate +3. **WAIT FOR INPUT**: If a menu is presented, halt and wait for user selection +4. **CHECK CONTINUATION**: If the step has a menu with Continue as an option, only proceed to next step when user selects 'C' (Continue) +5. **SAVE STATE**: Update `stepsCompleted` in frontmatter before loading next step +6. **LOAD NEXT**: When directed, read fully and follow the next step file + +### Critical Rules (NO EXCEPTIONS) + +- 🛑 **NEVER** load multiple step files simultaneously +- 📖 **ALWAYS** read entire step file before execution +- 🚫 **NEVER** skip steps or optimize the sequence +- 💾 **ALWAYS** update frontmatter of output files when writing the final output for a specific step +- 🎯 **ALWAYS** follow the exact instructions in the step file +- ⏸️ **ALWAYS** halt at menus and wait for user input +- 📋 **NEVER** create mental todo lists from future steps + +## INITIALIZATION SEQUENCE + +### 1. Configuration Loading + +Load and read full config from {main_config} and resolve: + +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as system-generated current datetime + +✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the configured `{communication_language}`. + +### 2. Route to Validate Workflow + +"**Validate Mode: Validating an existing PRD against BMAD standards.**" + +Prompt for PRD path: "Which PRD would you like to validate? Please provide the path to the PRD.md file." + +Then read fully and follow: `{validateWorkflow}` (steps-v/step-v-01-discovery.md) diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md b/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md deleted file mode 100644 index b13d7a7cf..000000000 --- a/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -name: create-prd -description: PRD tri-modal workflow - Create, Validate, or Edit comprehensive PRDs -main_config: '{project-root}/_bmad/bmm/config.yaml' -nextStep: './steps-c/step-01-init.md' -validateWorkflow: './steps-v/step-v-01-discovery.md' -editWorkflow: './steps-e/step-e-01-discovery.md' -web_bundle: true ---- - -# PRD Workflow (Tri-Modal) - -**Goal:** Create, Validate, or Edit comprehensive PRDs through structured workflows. - -**Your Role:** -- **Create Mode:** Product-focused PM facilitator collaborating with an expert peer -- **Validate Mode:** Validation Architect and Quality Assurance Specialist -- **Edit Mode:** PRD improvement specialist - -You will continue to operate with your given name, identity, and communication_style, merged with the details of this role description. - ---- - -## MODE DETERMINATION - -### Detect Workflow Mode - -Determine which mode to invoke based on: - -1. **Command/Invocation:** - - "create prd" or "new prd" → Create mode - - "validate prd" or "check prd" → Validate mode - - "edit prd" or "improve prd" → Edit mode - -2. **Context Detection:** - - If invoked with -c flag → Create mode - - If invoked with -v flag → Validate mode - - If invoked with -e flag → Edit mode - -3. **Menu Selection (if unclear):** - -If mode cannot be determined from invocation: -"**PRD Workflow - Select Mode:** - -**[C] Create** - Create a new PRD from scratch -**[V] Validate** - Validate an existing PRD against BMAD standards -**[E] Edit** - Improve an existing PRD - -Which mode would you like?" - -Wait for user selection. - -### Route to Appropriate Workflow - -**IF Create Mode:** -"**Create Mode: Creating a new PRD from scratch.**" -Read fully and follow: `{nextStep}` (steps-c/step-01-init.md) - -**IF Validate Mode:** -"**Validate Mode: Validating an existing PRD against BMAD standards.**" -Prompt for PRD path: "Which PRD would you like to validate? Please provide the path to the PRD.md file." -Then read fully and follow: `{validateWorkflow}` (steps-v/step-v-01-discovery.md) - -**IF Edit Mode:** -"**Edit Mode: Improving an existing PRD.**" -Prompt for PRD path: "Which PRD would you like to edit? Please provide the path to the PRD.md file." -Then read fully and follow: `{editWorkflow}` (steps-e/step-e-01-discovery.md) - ---- - -## WORKFLOW ARCHITECTURE - -This uses **step-file architecture** for disciplined execution: - -### Core Principles - -- **Micro-file Design**: Each step is a self contained instruction file that is a part of an overall workflow that must be followed exactly -- **Just-In-Time Loading**: Only the current step file is in memory - never load future step files until told to do so -- **Sequential Enforcement**: Sequence within the step files must be completed in order, no skipping or optimization allowed -- **State Tracking**: Document progress in output file frontmatter using `stepsCompleted` array when a workflow produces a document -- **Append-Only Building**: Build documents by appending content as directed to the output file - -### Step Processing Rules - -1. **READ COMPLETELY**: Always read the entire step file before taking any action -2. **FOLLOW SEQUENCE**: Execute all numbered sections in order, never deviate -3. **WAIT FOR INPUT**: If a menu is presented, halt and wait for user selection -4. **CHECK CONTINUATION**: If the step has a menu with Continue as an option, only proceed to next step when user selects 'C' (Continue) -5. **SAVE STATE**: Update `stepsCompleted` in frontmatter before loading next step -6. **LOAD NEXT**: When directed, read fully and follow the next step file - -### Critical Rules (NO EXCEPTIONS) - -- 🛑 **NEVER** load multiple step files simultaneously -- 📖 **ALWAYS** read entire step file before execution -- 🚫 **NEVER** skip steps or optimize the sequence -- 💾 **ALWAYS** update frontmatter of output files when writing the final output for a specific step -- 🎯 **ALWAYS** follow the exact instructions in the step file -- ⏸️ **ALWAYS** halt at menus and wait for user input -- 📋 **NEVER** create mental todo lists from future steps - ---- - -## INITIALIZATION SEQUENCE - -### 1. Mode Determination - -**Check if mode was specified in the command invocation:** - -- If user invoked with "create prd" or "new prd" or "build prd" or "-c" or "--create" → Set mode to **create** -- If user invoked with "validate prd" or "review prd" or "check prd" or "-v" or "--validate" → Set mode to **validate** -- If user invoked with "edit prd" or "modify prd" or "improve prd" or "-e" or "--edit" → Set mode to **edit** - -**If mode is still unclear, ask user:** - -"**PRD Workflow - Select Mode:** - -**[C] Create** - Create a new PRD from scratch -**[V] Validate** - Validate an existing PRD against BMAD standards -**[E] Edit** - Improve an existing PRD - -Which mode would you like?" - -Wait for user selection. - -### 2. Configuration Loading - -Load and read full config from {main_config} and resolve: - -- `project_name`, `output_folder`, `planning_artifacts`, `user_name` -- `communication_language`, `document_output_language`, `user_skill_level` -- `date` as system-generated current datetime - -✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the configured `{communication_language}`. - -### 3. Route to Appropriate Workflow - -**IF mode == create:** -"**Create Mode: Creating a new PRD from scratch.**" -Read fully and follow: `{nextStep}` (steps-c/step-01-init.md) - -**IF mode == validate:** -"**Validate Mode: Validating an existing PRD against BMAD standards.**" -Prompt for PRD path: "Which PRD would you like to validate? Please provide the path to the PRD.md file." -Then read fully and follow: `{validateWorkflow}` (steps-v/step-v-01-discovery.md) - -**IF mode == edit:** -"**Edit Mode: Improving an existing PRD.**" -Prompt for PRD path: "Which PRD would you like to edit? Please provide the path to the PRD.md file." -Then read fully and follow: `{editWorkflow}` (steps-e/step-e-01-discovery.md) diff --git a/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-14-complete.md b/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-14-complete.md index fe7847885..db25fb9b7 100644 --- a/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-14-complete.md +++ b/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-14-complete.md @@ -82,7 +82,7 @@ Update the main workflow status file: ### 3. Suggest Next Steps -UX Design complete. Read fully and follow: `_bmad/core/tasks/bmad-help.md` with argument `Create UX`. +UX Design complete. Read fully and follow: `_bmad/core/tasks/help.md` with argument `Create UX`. ### 5. Final Completion Confirmation diff --git a/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md b/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md index d74cb4878..4af87c39a 100644 --- a/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md +++ b/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md @@ -1,7 +1,6 @@ --- name: create-ux-design description: Work with a peer UX Design expert to plan your applications UX patterns, look and feel. -web_bundle: true --- # Create UX Design Workflow diff --git a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-01-document-discovery.md b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-01-document-discovery.md index fccb7da22..877193f3d 100644 --- a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-01-document-discovery.md +++ b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-01-document-discovery.md @@ -2,15 +2,9 @@ name: 'step-01-document-discovery' description: 'Discover and inventory all project documents, handling duplicates and organizing file structure' -# Path Definitions -workflow_path: '{project-root}/_bmad/bmm/workflows/3-solutioning/implementation-readiness' - -# File References -thisStepFile: './step-01-document-discovery.md' nextStepFile: './step-02-prd-analysis.md' -workflowFile: '{workflow_path}/workflow.md' outputFile: '{planning_artifacts}/implementation-readiness-report-{{date}}.md' -templateFile: '{workflow_path}/templates/readiness-report-template.md' +templateFile: '../templates/readiness-report-template.md' --- # Step 1: Document Discovery diff --git a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-02-prd-analysis.md b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-02-prd-analysis.md index 5dd08705a..4d22e7da9 100644 --- a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-02-prd-analysis.md +++ b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-02-prd-analysis.md @@ -2,13 +2,7 @@ name: 'step-02-prd-analysis' description: 'Read and analyze PRD to extract all FRs and NFRs for coverage validation' -# Path Definitions -workflow_path: '{project-root}/_bmad/bmm/workflows/3-solutioning/implementation-readiness' - -# File References -thisStepFile: './step-02-prd-analysis.md' nextStepFile: './step-03-epic-coverage-validation.md' -workflowFile: '{workflow_path}/workflow.md' outputFile: '{planning_artifacts}/implementation-readiness-report-{{date}}.md' epicsFile: '{planning_artifacts}/*epic*.md' # Will be resolved to actual file --- diff --git a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-03-epic-coverage-validation.md b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-03-epic-coverage-validation.md index 981a5b63a..b73511bea 100644 --- a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-03-epic-coverage-validation.md +++ b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-03-epic-coverage-validation.md @@ -2,13 +2,7 @@ name: 'step-03-epic-coverage-validation' description: 'Validate that all PRD FRs are covered in epics and stories' -# Path Definitions -workflow_path: '{project-root}/_bmad/bmm/workflows/3-solutioning/implementation-readiness' - -# File References -thisStepFile: './step-03-epic-coverage-validation.md' nextStepFile: './step-04-ux-alignment.md' -workflowFile: '{workflow_path}/workflow.md' outputFile: '{planning_artifacts}/implementation-readiness-report-{{date}}.md' --- diff --git a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-04-ux-alignment.md b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-04-ux-alignment.md index 33aad045d..236ad3b51 100644 --- a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-04-ux-alignment.md +++ b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-04-ux-alignment.md @@ -2,13 +2,7 @@ name: 'step-04-ux-alignment' description: 'Check for UX document and validate alignment with PRD and Architecture' -# Path Definitions -workflow_path: '{project-root}/_bmad/bmm/workflows/3-solutioning/implementation-readiness' - -# File References -thisStepFile: './step-04-ux-alignment.md' nextStepFile: './step-05-epic-quality-review.md' -workflowFile: '{workflow_path}/workflow.md' outputFile: '{planning_artifacts}/implementation-readiness-report-{{date}}.md' --- diff --git a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-05-epic-quality-review.md b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-05-epic-quality-review.md index 0203cdc1a..9f6d087f8 100644 --- a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-05-epic-quality-review.md +++ b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-05-epic-quality-review.md @@ -2,15 +2,8 @@ name: 'step-05-epic-quality-review' description: 'Validate epics and stories against create-epics-and-stories best practices' -# Path Definitions -workflow_path: '{project-root}/_bmad/bmm/workflows/3-solutioning/implementation-readiness' - -# File References -thisStepFile: './step-05-epic-quality-review.md' nextStepFile: './step-06-final-assessment.md' -workflowFile: '{workflow_path}/workflow.md' outputFile: '{planning_artifacts}/implementation-readiness-report-{{date}}.md' -epicsBestPractices: '{project-root}/_bmad/bmm/workflows/3-solutioning/create-epics-and-stories' --- # Step 5: Epic Quality Review diff --git a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-06-final-assessment.md b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-06-final-assessment.md index cc826ee95..d0e15bc02 100644 --- a/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-06-final-assessment.md +++ b/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-06-final-assessment.md @@ -2,12 +2,6 @@ name: 'step-06-final-assessment' description: 'Compile final assessment and polish the readiness report' -# Path Definitions -workflow_path: '{project-root}/_bmad/bmm/workflows/3-solutioning/implementation-readiness' - -# File References -thisStepFile: './step-06-final-assessment.md' -workflowFile: '{workflow_path}/workflow.md' outputFile: '{planning_artifacts}/implementation-readiness-report-{{date}}.md' --- @@ -115,7 +109,7 @@ The assessment found [number] issues requiring attention. Review the detailed re The implementation readiness workflow is now complete. The report contains all findings and recommendations for the user to consider. -Implementation Readiness complete. Read fully and follow: `_bmad/core/tasks/bmad-help.md` with argument `implementation readiness`. +Implementation Readiness complete. Read fully and follow: `_bmad/core/tasks/help.md` with argument `implementation readiness`. --- diff --git a/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md b/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md index d7eb5969e..49d2afab9 100644 --- a/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md +++ b/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md @@ -1,7 +1,6 @@ --- name: check-implementation-readiness description: 'Critical validation workflow that assesses PRD, Architecture, and Epics & Stories for completeness and alignment before implementation. Uses adversarial review approach to find gaps and issues.' -web_bundle: false --- # Implementation Readiness diff --git a/src/bmm/workflows/3-solutioning/create-architecture/steps/step-08-complete.md b/src/bmm/workflows/3-solutioning/create-architecture/steps/step-08-complete.md index f317bddf9..2f949bf7e 100644 --- a/src/bmm/workflows/3-solutioning/create-architecture/steps/step-08-complete.md +++ b/src/bmm/workflows/3-solutioning/create-architecture/steps/step-08-complete.md @@ -41,7 +41,7 @@ completedAt: '{{current_date}}' ### 3. Next Steps Guidance -Architecture complete. Read fully and follow: `_bmad/core/tasks/bmad-help.md` with argument `Create Architecture`. +Architecture complete. Read fully and follow: `_bmad/core/tasks/help.md` with argument `Create Architecture`. Upon Completion of task output: offer to answer any questions about the Architecture Document. diff --git a/src/bmm/workflows/3-solutioning/create-architecture/workflow.md b/src/bmm/workflows/3-solutioning/create-architecture/workflow.md index d36c328e8..b75b4a46c 100644 --- a/src/bmm/workflows/3-solutioning/create-architecture/workflow.md +++ b/src/bmm/workflows/3-solutioning/create-architecture/workflow.md @@ -1,7 +1,6 @@ --- name: create-architecture description: Collaborative architectural decision facilitation for AI-agent consistency. Replaces template-driven architecture with intelligent, adaptive conversation that produces a decision-focused architecture document optimized for preventing agent conflicts. -web_bundle: true --- # Architecture Workflow diff --git a/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-04-final-validation.md b/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-04-final-validation.md index 19aa73d1f..05e8d5d4e 100644 --- a/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-04-final-validation.md +++ b/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-04-final-validation.md @@ -144,6 +144,6 @@ If all validations pass: When C is selected, the workflow is complete and the epics.md is ready for development. -Epics and Stories complete. Read fully and follow: `_bmad/core/tasks/bmad-help.md` with argument `Create Epics and Stories`. +Epics and Stories complete. Read fully and follow: `_bmad/core/tasks/help.md` with argument `Create Epics and Stories`. Upon Completion of task output: offer to answer any questions about the Epics and Stories. diff --git a/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md b/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md index a1e78a028..a0e232ab8 100644 --- a/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md +++ b/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md @@ -1,7 +1,6 @@ --- name: create-epics-and-stories description: 'Transform PRD requirements and Architecture decisions into comprehensive stories organized by user value. This workflow requires completed PRD + Architecture documents (UX recommended if UI exists) and breaks down requirements into implementation-ready epics and user stories that incorporate all available technical and design context. Creates detailed, actionable stories with complete acceptance criteria for development teams.' -web_bundle: true --- # Create Epics and Stories diff --git a/src/bmm/workflows/4-implementation/code-review/workflow.yaml b/src/bmm/workflows/4-implementation/code-review/workflow.yaml index 9e66b9325..5b5f6b2fc 100644 --- a/src/bmm/workflows/4-implementation/code-review/workflow.yaml +++ b/src/bmm/workflows/4-implementation/code-review/workflow.yaml @@ -46,6 +46,3 @@ input_file_patterns: sharded_index: "{planning_artifacts}/*epic*/index.md" sharded_single: "{planning_artifacts}/*epic*/epic-{{epic_num}}.md" load_strategy: "SELECTIVE_LOAD" - -standalone: true -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/correct-course/workflow.yaml b/src/bmm/workflows/4-implementation/correct-course/workflow.yaml index 70813514a..318b5a7dc 100644 --- a/src/bmm/workflows/4-implementation/correct-course/workflow.yaml +++ b/src/bmm/workflows/4-implementation/correct-course/workflow.yaml @@ -54,7 +54,3 @@ instructions: "{installed_path}/instructions.md" validation: "{installed_path}/checklist.md" checklist: "{installed_path}/checklist.md" default_output_file: "{planning_artifacts}/sprint-change-proposal-{date}.md" - -standalone: true - -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/create-story/workflow.yaml b/src/bmm/workflows/4-implementation/create-story/workflow.yaml index 258794c7c..1f3ac9784 100644 --- a/src/bmm/workflows/4-implementation/create-story/workflow.yaml +++ b/src/bmm/workflows/4-implementation/create-story/workflow.yaml @@ -55,7 +55,3 @@ input_file_patterns: whole: "{planning_artifacts}/*epic*.md" sharded: "{planning_artifacts}/*epic*/*.md" load_strategy: "SELECTIVE_LOAD" # Only load needed epic - -standalone: true - -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/dev-story/workflow.yaml b/src/bmm/workflows/4-implementation/dev-story/workflow.yaml index d5824ee17..daf152b71 100644 --- a/src/bmm/workflows/4-implementation/dev-story/workflow.yaml +++ b/src/bmm/workflows/4-implementation/dev-story/workflow.yaml @@ -21,7 +21,3 @@ story_file: "" # Explicit story path; auto-discovered if empty implementation_artifacts: "{config_source}:implementation_artifacts" sprint_status: "{implementation_artifacts}/sprint-status.yaml" project_context: "**/project-context.md" - -standalone: true - -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/retrospective/workflow.yaml b/src/bmm/workflows/4-implementation/retrospective/workflow.yaml index 80d934b2c..b92ecaf1b 100644 --- a/src/bmm/workflows/4-implementation/retrospective/workflow.yaml +++ b/src/bmm/workflows/4-implementation/retrospective/workflow.yaml @@ -53,6 +53,3 @@ input_file_patterns: sprint_status_file: "{implementation_artifacts}/sprint-status.yaml" story_directory: "{implementation_artifacts}" retrospectives_folder: "{implementation_artifacts}" - -standalone: true -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml b/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml index 25ccf5f72..7b157633c 100644 --- a/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml +++ b/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml @@ -49,7 +49,3 @@ input_file_patterns: # Output configuration default_output_file: "{status_file}" - -standalone: true - -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml b/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml index 6f10a9a67..8946f0291 100644 --- a/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml +++ b/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml @@ -28,9 +28,3 @@ input_file_patterns: description: "Sprint status file generated by sprint-planning" whole: "{implementation_artifacts}/sprint-status.yaml" load_strategy: "FULL_LOAD" - -# Standalone so IDE commands get generated -standalone: true - -# No web bundle needed -web_bundle: false diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-01-mode-detection.md b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-01-mode-detection.md index 4ea630b19..eb3458891 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-01-mode-detection.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-01-mode-detection.md @@ -2,8 +2,6 @@ name: 'step-01-mode-detection' description: 'Determine execution mode (tech-spec vs direct), handle escalation, set state variables' -workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev' -thisStepFile: './step-01-mode-detection.md' nextStepFile_modeA: './step-03-execute.md' nextStepFile_modeB: './step-02-context-gathering.md' --- diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-02-context-gathering.md b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-02-context-gathering.md index dffb86a8c..d3461bb16 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-02-context-gathering.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-02-context-gathering.md @@ -2,8 +2,6 @@ name: 'step-02-context-gathering' description: 'Quick context gathering for direct mode - identify files, patterns, dependencies' -workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev' -thisStepFile: './step-02-context-gathering.md' nextStepFile: './step-03-execute.md' --- diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-03-execute.md b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-03-execute.md index 9d7283614..baeab834b 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-03-execute.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-03-execute.md @@ -2,8 +2,6 @@ name: 'step-03-execute' description: 'Execute implementation - iterate through tasks, write code, run tests' -workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev' -thisStepFile: './step-03-execute.md' nextStepFile: './step-04-self-check.md' --- diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-04-self-check.md b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-04-self-check.md index 6179ebba4..0c6a822c3 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-04-self-check.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-04-self-check.md @@ -2,8 +2,6 @@ name: 'step-04-self-check' description: 'Self-audit implementation against tasks, tests, AC, and patterns' -workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev' -thisStepFile: './step-04-self-check.md' nextStepFile: './step-05-adversarial-review.md' --- diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-05-adversarial-review.md b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-05-adversarial-review.md index 50c786d04..41c8f4741 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-05-adversarial-review.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-05-adversarial-review.md @@ -2,8 +2,6 @@ name: 'step-05-adversarial-review' description: 'Construct diff and invoke adversarial review task' -workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev' -thisStepFile: './step-05-adversarial-review.md' nextStepFile: './step-06-resolve-findings.md' --- @@ -59,7 +57,7 @@ Merge all changes into `{diff_output}`. ### 2. Invoke Adversarial Review -With `{diff_output}` constructed, invoke the review task. If possible, use information asymmetry: run this step, and only it, in a separate subagent or process with read access to the project, but no context except the `{diff_output}`. +With `{diff_output}` constructed, load and follow the review task. If possible, use information asymmetry: load this step, and only it, in a separate subagent or process with read access to the project, but no context except the `{diff_output}`. ```xml Review {diff_output} using {project-root}/_bmad/core/tasks/review-adversarial-general.xml diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-06-resolve-findings.md b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-06-resolve-findings.md index 4ab367c62..5c9165c86 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-06-resolve-findings.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-06-resolve-findings.md @@ -1,9 +1,6 @@ --- name: 'step-06-resolve-findings' description: 'Handle review findings interactively, apply fixes, update tech-spec with final status' - -workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev' -thisStepFile: './step-06-resolve-findings.md' --- # Step 6: Resolve Findings diff --git a/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-01-understand.md b/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-01-understand.md index a7cde5558..d338f24b7 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-01-understand.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-01-understand.md @@ -2,10 +2,9 @@ name: 'step-01-understand' description: 'Analyze the requirement delta between current state and what user wants to build' -workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-spec' nextStepFile: './step-02-investigate.md' skipToStepFile: './step-03-generate.md' -templateFile: '{workflow_path}/tech-spec-template.md' +templateFile: '../tech-spec-template.md' wipFile: '{implementation_artifacts}/tech-spec-wip.md' --- diff --git a/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-02-investigate.md b/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-02-investigate.md index 1b0d0ceeb..533c0d55b 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-02-investigate.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-02-investigate.md @@ -2,7 +2,6 @@ name: 'step-02-investigate' description: 'Map technical constraints and anchor points within the codebase' -workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-spec' nextStepFile: './step-03-generate.md' wipFile: '{implementation_artifacts}/tech-spec-wip.md' --- diff --git a/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-03-generate.md b/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-03-generate.md index 79999db39..1a163ccb0 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-03-generate.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-03-generate.md @@ -2,7 +2,6 @@ name: 'step-03-generate' description: 'Build the implementation plan based on the technical mapping of constraints' -workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-spec' nextStepFile: './step-04-review.md' wipFile: '{implementation_artifacts}/tech-spec-wip.md' --- diff --git a/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md b/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md index a223a2e4f..24c65d088 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md @@ -2,7 +2,6 @@ name: 'step-04-review' description: 'Review and finalize the tech-spec' -workflow_path: '{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-spec' wipFile: '{implementation_artifacts}/tech-spec-wip.md' --- @@ -153,7 +152,7 @@ b) **HALT and wait for user selection.** #### Adversarial Review [R] Process: 1. **Invoke Adversarial Review Task**: - > With `{finalFile}` constructed, invoke the review task. If possible, use information asymmetry: run this task, and only it, in a separate subagent or process with read access to the project, but no context except the `{finalFile}`. + > With `{finalFile}` constructed, load and follow the review task. If possible, use information asymmetry: load this task, and only it, in a separate subagent or process with read access to the project, but no context except the `{finalFile}`. Review {finalFile} using {project-root}/_bmad/core/tasks/review-adversarial-general.xml > **Platform fallback:** If task invocation not available, load the task file and follow its instructions inline, passing `{finalFile}` as the content. > The task should: review `{finalFile}` and return a list of findings. diff --git a/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md b/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md index bb6c877a7..7c41b948d 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md @@ -2,7 +2,6 @@ name: quick-spec description: Conversational spec engineering - ask questions, investigate code, produce implementation-ready tech-spec. main_config: '{project-root}/_bmad/bmm/config.yaml' -web_bundle: true # Checkpoint handler paths advanced_elicitation: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml' diff --git a/src/bmm/workflows/document-project/workflow.yaml b/src/bmm/workflows/document-project/workflow.yaml index 536257b3d..4667d7c0b 100644 --- a/src/bmm/workflows/document-project/workflow.yaml +++ b/src/bmm/workflows/document-project/workflow.yaml @@ -20,11 +20,3 @@ validation: "{installed_path}/checklist.md" # Required data files - CRITICAL for project type detection and documentation requirements documentation_requirements_csv: "{installed_path}/documentation-requirements.csv" - -# Output configuration - Multiple files generated in output folder -# Primary output: {output_folder}/project-documentation/ -# Additional files generated by sub-workflows based on project structure - -standalone: true - -web_bundle: false diff --git a/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json b/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json deleted file mode 100644 index d18f94af3..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "type": "excalidrawlib", - "version": 2, - "library": [ - { - "id": "start-end-circle", - "status": "published", - "elements": [ - { - "type": "ellipse", - "width": 120, - "height": 60, - "strokeColor": "#1976d2", - "backgroundColor": "#e3f2fd", - "fillStyle": "solid", - "strokeWidth": 2, - "roughness": 0 - } - ] - }, - { - "id": "process-rectangle", - "status": "published", - "elements": [ - { - "type": "rectangle", - "width": 160, - "height": 80, - "strokeColor": "#1976d2", - "backgroundColor": "#e3f2fd", - "fillStyle": "solid", - "strokeWidth": 2, - "roughness": 0, - "roundness": { - "type": 3, - "value": 8 - } - } - ] - }, - { - "id": "decision-diamond", - "status": "published", - "elements": [ - { - "type": "diamond", - "width": 140, - "height": 100, - "strokeColor": "#f57c00", - "backgroundColor": "#fff3e0", - "fillStyle": "solid", - "strokeWidth": 2, - "roughness": 0 - } - ] - }, - { - "id": "data-store", - "status": "published", - "elements": [ - { - "type": "rectangle", - "width": 140, - "height": 80, - "strokeColor": "#388e3c", - "backgroundColor": "#e8f5e9", - "fillStyle": "solid", - "strokeWidth": 2, - "roughness": 0 - } - ] - }, - { - "id": "external-entity", - "status": "published", - "elements": [ - { - "type": "rectangle", - "width": 120, - "height": 80, - "strokeColor": "#7b1fa2", - "backgroundColor": "#f3e5f5", - "fillStyle": "solid", - "strokeWidth": 3, - "roughness": 0 - } - ] - } - ] -} diff --git a/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml b/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml deleted file mode 100644 index 6fab2a3d7..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml +++ /dev/null @@ -1,127 +0,0 @@ -flowchart: - viewport: - x: 0 - y: 0 - zoom: 1 - grid: - size: 20 - spacing: - vertical: 100 - horizontal: 180 - elements: - start: - type: ellipse - width: 120 - height: 60 - label: "Start" - process: - type: rectangle - width: 160 - height: 80 - roundness: 8 - decision: - type: diamond - width: 140 - height: 100 - end: - type: ellipse - width: 120 - height: 60 - label: "End" - -diagram: - viewport: - x: 0 - y: 0 - zoom: 1 - grid: - size: 20 - spacing: - vertical: 120 - horizontal: 200 - elements: - component: - type: rectangle - width: 180 - height: 100 - roundness: 8 - database: - type: rectangle - width: 140 - height: 80 - service: - type: rectangle - width: 160 - height: 90 - roundness: 12 - external: - type: rectangle - width: 140 - height: 80 - -wireframe: - viewport: - x: 0 - y: 0 - zoom: 0.8 - grid: - size: 20 - spacing: - vertical: 40 - horizontal: 40 - elements: - container: - type: rectangle - width: 800 - height: 600 - strokeStyle: solid - strokeWidth: 2 - header: - type: rectangle - width: 800 - height: 80 - button: - type: rectangle - width: 120 - height: 40 - roundness: 4 - input: - type: rectangle - width: 300 - height: 40 - roundness: 4 - text: - type: text - fontSize: 16 - -dataflow: - viewport: - x: 0 - y: 0 - zoom: 1 - grid: - size: 20 - spacing: - vertical: 120 - horizontal: 200 - elements: - process: - type: ellipse - width: 140 - height: 80 - label: "Process" - datastore: - type: rectangle - width: 140 - height: 80 - label: "Data Store" - external: - type: rectangle - width: 120 - height: 80 - strokeWidth: 3 - label: "External Entity" - dataflow: - type: arrow - strokeWidth: 2 - label: "Data Flow" diff --git a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md b/src/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md deleted file mode 100644 index 3c9463d5d..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md +++ /dev/null @@ -1,39 +0,0 @@ -# Create Data Flow Diagram - Validation Checklist - -## DFD Notation - -- [ ] Processes shown as circles/ellipses -- [ ] Data stores shown as parallel lines or rectangles -- [ ] External entities shown as rectangles -- [ ] Data flows shown as labeled arrows -- [ ] Follows standard DFD notation - -## Structure - -- [ ] All processes numbered correctly -- [ ] All data flows labeled with data names -- [ ] All data stores named appropriately -- [ ] External entities clearly identified - -## Completeness - -- [ ] All inputs and outputs accounted for -- [ ] No orphaned processes (unconnected) -- [ ] Data conservation maintained -- [ ] Level appropriate (context/level 0/level 1) - -## Layout - -- [ ] Logical flow direction (left to right, top to bottom) -- [ ] No crossing data flows where avoidable -- [ ] Balanced layout -- [ ] Grid alignment maintained - -## Technical Quality - -- [ ] All elements properly grouped -- [ ] Arrows have proper bindings -- [ ] Text readable and properly sized -- [ ] No elements with `isDeleted: true` -- [ ] JSON is valid -- [ ] File saved to correct location diff --git a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md b/src/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md deleted file mode 100644 index 30d32ed33..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md +++ /dev/null @@ -1,130 +0,0 @@ -# Create Data Flow Diagram - Workflow Instructions - -```xml -The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml -You MUST have already loaded and processed: {installed_path}/workflow.yaml -This workflow creates data flow diagrams (DFD) in Excalidraw format. - - - - - Review user's request and extract: DFD level, processes, data stores, external entities - Skip to Step 4 - - - - Ask: "What level of DFD do you need?" - Present options: - 1. Context Diagram (Level 0) - Single process showing system boundaries - 2. Level 1 DFD - Major processes and data flows - 3. Level 2 DFD - Detailed sub-processes - 4. Custom - Specify your requirements - - WAIT for selection - - - - Ask: "Describe the processes, data stores, and external entities in your system" - WAIT for user description - Summarize what will be included and confirm with user - - - - Check for existing theme.json, ask to use if exists - - Ask: "Choose a DFD color scheme:" - Present numbered options: - 1. Standard DFD - - Process: #e3f2fd (light blue) - - Data Store: #e8f5e9 (light green) - - External Entity: #f3e5f5 (light purple) - - Border: #1976d2 (blue) - - 2. Colorful DFD - - Process: #fff9c4 (light yellow) - - Data Store: #c5e1a5 (light lime) - - External Entity: #ffccbc (light coral) - - Border: #f57c00 (orange) - - 3. Minimal DFD - - Process: #f5f5f5 (light gray) - - Data Store: #eeeeee (gray) - - External Entity: #e0e0e0 (medium gray) - - Border: #616161 (dark gray) - - 4. Custom - Define your own colors - - WAIT for selection - Create theme.json based on selection - - - - - List all processes with numbers (1.0, 2.0, etc.) - List all data stores (D1, D2, etc.) - List all external entities - Map all data flows with labels - Show planned structure, confirm with user - - - - Load {{templates}} and extract `dataflow` section - Load {{library}} - Load theme.json - Load {{helpers}} - - - - Follow standard DFD notation from {{helpers}} - - Build Order: - 1. External entities (rectangles, bold border) - 2. Processes (circles/ellipses with numbers) - 3. Data stores (parallel lines or rectangles) - 4. Data flows (labeled arrows) - - - DFD Rules: - - Processes: Numbered (1.0, 2.0), verb phrases - - Data stores: Named (D1, D2), noun phrases - - External entities: Named, noun phrases - - Data flows: Labeled with data names, arrows show direction - - No direct flow between external entities - - No direct flow between data stores - - - Layout: - - External entities at edges - - Processes in center - - Data stores between processes - - Minimize crossing flows - - Left-to-right or top-to-bottom flow - - - - - Verify DFD rules compliance - Strip unused elements and elements with isDeleted: true - Save to {{default_output_file}} - - - - NEVER delete the file if validation fails - always fix syntax errors - Run: node -e "JSON.parse(require('fs').readFileSync('{{default_output_file}}', 'utf8')); console.log('✓ Valid JSON')" - - Read the error message carefully - it shows the syntax error and position - Open the file and navigate to the error location - Fix the syntax error (add missing comma, bracket, or quote as indicated) - Save the file - Re-run validation with the same command - Repeat until validation passes - - Once validation passes, confirm with user - - - - Validate against {{validation}} - - - -``` diff --git a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml b/src/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml deleted file mode 100644 index 2f01e6b51..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: create-excalidraw-dataflow -description: "Create data flow diagrams (DFD) in Excalidraw format" -author: "BMad" - -# Config values -config_source: "{project-root}/_bmad/bmm/config.yaml" -output_folder: "{config_source}:output_folder" - -# Workflow components -installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow" -shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared" -instructions: "{installed_path}/instructions.md" -validation: "{installed_path}/checklist.md" - -# Core Excalidraw resources (universal knowledge) -helpers: "{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md" -json_validation: "{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md" - -# Domain-specific resources (technical diagrams) -templates: "{shared_path}/excalidraw-templates.yaml" -library: "{shared_path}/excalidraw-library.json" - -# Output file (respects user's configured output_folder) -default_output_file: "{output_folder}/excalidraw-diagrams/dataflow-{timestamp}.excalidraw" - -standalone: true -web_bundle: false diff --git a/src/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md b/src/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md deleted file mode 100644 index 61d216aea..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md +++ /dev/null @@ -1,43 +0,0 @@ -# Create Diagram - Validation Checklist - -## Element Structure - -- [ ] All components with labels have matching `groupIds` -- [ ] All text elements have `containerId` pointing to parent component -- [ ] Text width calculated properly (no cutoff) -- [ ] Text alignment appropriate for diagram type - -## Layout and Alignment - -- [ ] All elements snapped to 20px grid -- [ ] Component spacing consistent (40px/60px) -- [ ] Hierarchical alignment maintained -- [ ] No overlapping elements - -## Connections - -- [ ] All arrows have `startBinding` and `endBinding` -- [ ] `boundElements` array updated on connected components -- [ ] Arrow routing avoids overlaps -- [ ] Relationship types clearly indicated - -## Notation and Standards - -- [ ] Follows specified notation standard (UML/ERD/etc) -- [ ] Symbols used correctly -- [ ] Cardinality/multiplicity shown where needed -- [ ] Labels and annotations clear - -## Theme and Styling - -- [ ] Theme colors applied consistently -- [ ] Component types visually distinguishable -- [ ] Text is readable -- [ ] Professional appearance - -## Output Quality - -- [ ] Element count under 80 -- [ ] No elements with `isDeleted: true` -- [ ] JSON is valid -- [ ] File saved to correct location diff --git a/src/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md b/src/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md deleted file mode 100644 index 407a76bf7..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md +++ /dev/null @@ -1,141 +0,0 @@ -# Create Diagram - Workflow Instructions - -```xml -The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml -You MUST have already loaded and processed: {installed_path}/workflow.yaml -This workflow creates system architecture diagrams, ERDs, UML diagrams, or general technical diagrams in Excalidraw format. - - - - - Review user's request and extract: diagram type, components/entities, relationships, notation preferences - Skip to Step 5 - Only ask about missing info in Steps 1-2 - - - - Ask: "What type of technical diagram do you need?" - Present options: - 1. System Architecture - 2. Entity-Relationship Diagram (ERD) - 3. UML Class Diagram - 4. UML Sequence Diagram - 5. UML Use Case Diagram - 6. Network Diagram - 7. Other - - WAIT for selection - - - - Ask: "Describe the components/entities and their relationships" - Ask: "What notation standard? (Standard/Simplified/Strict UML-ERD)" - WAIT for user input - Summarize what will be included and confirm with user - - - - Check if theme.json exists at output location - Ask to use it, load if yes, else proceed to Step 4 - Proceed to Step 4 - - - - Ask: "Choose a color scheme for your diagram:" - Present numbered options: - 1. Professional - - Component: #e3f2fd (light blue) - - Database: #e8f5e9 (light green) - - Service: #fff3e0 (light orange) - - Border: #1976d2 (blue) - - 2. Colorful - - Component: #e1bee7 (light purple) - - Database: #c5e1a5 (light lime) - - Service: #ffccbc (light coral) - - Border: #7b1fa2 (purple) - - 3. Minimal - - Component: #f5f5f5 (light gray) - - Database: #eeeeee (gray) - - Service: #e0e0e0 (medium gray) - - Border: #616161 (dark gray) - - 4. Custom - Define your own colors - - WAIT for selection - Create theme.json based on selection - Show preview and confirm - - - - List all components/entities - Map all relationships - Show planned layout - Ask: "Structure looks correct? (yes/no)" - Adjust and repeat - - - - Load {{templates}} and extract `diagram` section - Load {{library}} - Load theme.json and merge with template - Load {{helpers}} for guidelines - - - - Follow {{helpers}} for proper element creation - - For Each Component: - - Generate unique IDs (component-id, text-id, group-id) - - Create shape with groupIds - - Calculate text width - - Create text with containerId and matching groupIds - - Add boundElements - - - For Each Connection: - - Determine arrow type (straight/elbow) - - Create with startBinding and endBinding - - Update boundElements on both components - - - Build Order by Type: - - Architecture: Services → Databases → Connections → Labels - - ERD: Entities → Attributes → Relationships → Cardinality - - UML Class: Classes → Attributes → Methods → Relationships - - UML Sequence: Actors → Lifelines → Messages → Returns - - UML Use Case: Actors → Use Cases → Relationships - - - Alignment: - - Snap to 20px grid - - Space: 40px between components, 60px between sections - - - - - Strip unused elements and elements with isDeleted: true - Save to {{default_output_file}} - - - - NEVER delete the file if validation fails - always fix syntax errors - Run: node -e "JSON.parse(require('fs').readFileSync('{{default_output_file}}', 'utf8')); console.log('✓ Valid JSON')" - - Read the error message carefully - it shows the syntax error and position - Open the file and navigate to the error location - Fix the syntax error (add missing comma, bracket, or quote as indicated) - Save the file - Re-run validation with the same command - Repeat until validation passes - - Once validation passes, confirm: "Diagram created at {{default_output_file}}. Open to view?" - - - - Validate against {{validation}} using {_bmad}/core/tasks/validate-workflow.xml - - - -``` diff --git a/src/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml b/src/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml deleted file mode 100644 index f841a546f..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: create-excalidraw-diagram -description: "Create system architecture diagrams, ERDs, UML diagrams, or general technical diagrams in Excalidraw format" -author: "BMad" - -# Config values -config_source: "{project-root}/_bmad/bmm/config.yaml" -output_folder: "{config_source}:output_folder" - -# Workflow components -installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-diagram" -shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared" -instructions: "{installed_path}/instructions.md" -validation: "{installed_path}/checklist.md" - -# Core Excalidraw resources (universal knowledge) -helpers: "{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md" -json_validation: "{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md" - -# Domain-specific resources (technical diagrams) -templates: "{shared_path}/excalidraw-templates.yaml" -library: "{shared_path}/excalidraw-library.json" - -# Output file (respects user's configured output_folder) -default_output_file: "{output_folder}/excalidraw-diagrams/diagram-{timestamp}.excalidraw" - -standalone: true -web_bundle: false diff --git a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md b/src/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md deleted file mode 100644 index 7da7fb78d..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md +++ /dev/null @@ -1,49 +0,0 @@ -# Create Flowchart - Validation Checklist - -## Element Structure - -- [ ] All shapes with labels have matching `groupIds` -- [ ] All text elements have `containerId` pointing to parent shape -- [ ] Text width calculated properly (no cutoff) -- [ ] Text alignment set (`textAlign` + `verticalAlign`) - -## Layout and Alignment - -- [ ] All elements snapped to 20px grid -- [ ] Consistent spacing between elements (60px minimum) -- [ ] Vertical alignment maintained for flow direction -- [ ] No overlapping elements - -## Connections - -- [ ] All arrows have `startBinding` and `endBinding` -- [ ] `boundElements` array updated on connected shapes -- [ ] Arrow types appropriate (straight for forward, elbow for backward/upward) -- [ ] Gap set to 10 for all bindings - -## Theme and Styling - -- [ ] Theme colors applied consistently -- [ ] All shapes use theme primary fill color -- [ ] All borders use theme accent color -- [ ] Text color is readable (#1e1e1e) - -## Composition - -- [ ] Element count under 50 -- [ ] Library components referenced where possible -- [ ] No duplicate element definitions - -## Output Quality - -- [ ] No elements with `isDeleted: true` -- [ ] JSON is valid -- [ ] File saved to correct location - -## Functional Requirements - -- [ ] Start point clearly marked -- [ ] End point clearly marked -- [ ] All process steps labeled -- [ ] Decision points use diamond shapes -- [ ] Flow direction is clear and logical diff --git a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md b/src/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md deleted file mode 100644 index 742679050..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md +++ /dev/null @@ -1,241 +0,0 @@ -# Create Flowchart - Workflow Instructions - -```xml -The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml -You MUST have already loaded and processed: {installed_path}/workflow.yaml -This workflow creates a flowchart visualization in Excalidraw format for processes, pipelines, or logic flows. - - - - - Before asking any questions, analyze what the user has already told you - - Review the user's initial request and conversation history - Extract any mentioned: flowchart type, complexity, decision points, save location - - - Summarize your understanding - Skip directly to Step 4 (Plan Flowchart Layout) - - - - Note what you already know - Only ask about missing information in Step 1 - - - - Proceed with full elicitation in Step 1 - - - - - Ask Question 1: "What type of process flow do you need to visualize?" - Present numbered options: - 1. Business Process Flow - Document business workflows, approval processes, or operational procedures - 2. Algorithm/Logic Flow - Visualize code logic, decision trees, or computational processes - 3. User Journey Flow - Map user interactions, navigation paths, or experience flows - 4. Data Processing Pipeline - Show data transformation, ETL processes, or processing stages - 5. Other - Describe your specific flowchart needs - - WAIT for user selection (1-5) - - Ask Question 2: "How many main steps are in this flow?" - Present numbered options: - 1. Simple (3-5 steps) - Quick process with few decision points - 2. Medium (6-10 steps) - Standard workflow with some branching - 3. Complex (11-20 steps) - Detailed process with multiple decision points - 4. Very Complex (20+ steps) - Comprehensive workflow requiring careful layout - - WAIT for user selection (1-4) - Store selection in {{complexity}} - - Ask Question 3: "Does your flow include decision points (yes/no branches)?" - Present numbered options: - 1. No decisions - Linear flow from start to end - 2. Few decisions (1-2) - Simple branching with yes/no paths - 3. Multiple decisions (3-5) - Several conditional branches - 4. Complex decisions (6+) - Extensive branching logic - - WAIT for user selection (1-4) - Store selection in {{decision_points}} - - Ask Question 4: "Where should the flowchart be saved?" - Present numbered options: - 1. Default location - docs/flowcharts/[auto-generated-name].excalidraw - 2. Custom path - Specify your own file path - 3. Project root - Save in main project directory - 4. Specific folder - Choose from existing folders - - WAIT for user selection (1-4) - - Ask for specific path - WAIT for user input - - Store final path in {{default_output_file}} - - - - Check if theme.json exists at output location - - Ask: "Found existing theme. Use it? (yes/no)" - WAIT for user response - - Load and use existing theme - Skip to Step 4 - - - Proceed to Step 3 - - - - Proceed to Step 3 - - - - - Ask: "Let's create a theme for your flowchart. Choose a color scheme:" - Present numbered options: - 1. Professional Blue - - Primary Fill: #e3f2fd (light blue) - - Accent/Border: #1976d2 (blue) - - Decision: #fff3e0 (light orange) - - Text: #1e1e1e (dark gray) - - 2. Success Green - - Primary Fill: #e8f5e9 (light green) - - Accent/Border: #388e3c (green) - - Decision: #fff9c4 (light yellow) - - Text: #1e1e1e (dark gray) - - 3. Neutral Gray - - Primary Fill: #f5f5f5 (light gray) - - Accent/Border: #616161 (gray) - - Decision: #e0e0e0 (medium gray) - - Text: #1e1e1e (dark gray) - - 4. Warm Orange - - Primary Fill: #fff3e0 (light orange) - - Accent/Border: #f57c00 (orange) - - Decision: #ffe0b2 (peach) - - Text: #1e1e1e (dark gray) - - 5. Custom Colors - Define your own color palette - - WAIT for user selection (1-5) - Store selection in {{theme_choice}} - - - Ask: "Primary fill color (hex code)?" - WAIT for user input - Store in {{custom_colors.primary_fill}} - Ask: "Accent/border color (hex code)?" - WAIT for user input - Store in {{custom_colors.accent}} - Ask: "Decision color (hex code)?" - WAIT for user input - Store in {{custom_colors.decision}} - - - Create theme.json with selected colors - Show theme preview with all colors - Ask: "Theme looks good?" - Present numbered options: - 1. Yes, use this theme - Proceed with theme - 2. No, adjust colors - Modify color selections - 3. Start over - Choose different preset - - WAIT for selection (1-3) - - Repeat Step 3 - - - - - List all steps and decision points based on gathered requirements - Show user the planned structure - Ask: "Structure looks correct? (yes/no)" - WAIT for user response - - Adjust structure based on feedback - Repeat this step - - - - - Load {{templates}} file - Extract `flowchart` section from YAML - Load {{library}} file - Load theme.json and merge colors with template - Load {{helpers}} for element creation guidelines - - - - Follow guidelines from {{helpers}} for proper element creation - - Build ONE section at a time following these rules: - - For Each Shape with Label: - 1. Generate unique IDs (shape-id, text-id, group-id) - 2. Create shape with groupIds: [group-id] - 3. Calculate text width: (text.length × fontSize × 0.6) + 20, round to nearest 10 - 4. Create text element with: - - containerId: shape-id - - groupIds: [group-id] (SAME as shape) - - textAlign: "center" - - verticalAlign: "middle" - - width: calculated width - 5. Add boundElements to shape referencing text - - - For Each Arrow: - 1. Determine arrow type needed: - - Straight: For forward flow (left-to-right, top-to-bottom) - - Elbow: For upward flow, backward flow, or complex routing - 2. Create arrow with startBinding and endBinding - 3. Set startBinding.elementId to source shape ID - 4. Set endBinding.elementId to target shape ID - 5. Set gap: 10 for both bindings - 6. If elbow arrow, add intermediate points for direction changes - 7. Update boundElements on both connected shapes - - - Alignment: - - Snap all x, y to 20px grid - - Align shapes vertically (same x for vertical flow) - - Space elements: 60px between shapes - - - Build Order: - 1. Start point (circle) with label - 2. Each process step (rectangle) with label - 3. Each decision point (diamond) with label - 4. End point (circle) with label - 5. Connect all with bound arrows - - - - - Strip unused elements and elements with isDeleted: true - Save to {{default_output_file}} - - - - NEVER delete the file if validation fails - always fix syntax errors - Run: node -e "JSON.parse(require('fs').readFileSync('{{default_output_file}}', 'utf8')); console.log('✓ Valid JSON')" - - Read the error message carefully - it shows the syntax error and position - Open the file and navigate to the error location - Fix the syntax error (add missing comma, bracket, or quote as indicated) - Save the file - Re-run validation with the same command - Repeat until validation passes - - Once validation passes, confirm with user: "Flowchart created at {{default_output_file}}. Open to view?" - - - - Validate against checklist at {{validation}} using {_bmad}/core/tasks/validate-workflow.xml - - - -``` diff --git a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml b/src/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml deleted file mode 100644 index 6079d6de2..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: create-excalidraw-flowchart -description: "Create a flowchart visualization in Excalidraw format for processes, pipelines, or logic flows" -author: "BMad" - -# Config values -config_source: "{project-root}/_bmad/bmm/config.yaml" -output_folder: "{config_source}:output_folder" - -# Workflow components -installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-flowchart" -shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared" -instructions: "{installed_path}/instructions.md" -validation: "{installed_path}/checklist.md" - -# Core Excalidraw resources (universal knowledge) -helpers: "{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md" -json_validation: "{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md" - -# Domain-specific resources (technical diagrams) -templates: "{shared_path}/excalidraw-templates.yaml" -library: "{shared_path}/excalidraw-library.json" - -# Output file (respects user's configured output_folder) -default_output_file: "{output_folder}/excalidraw-diagrams/flowchart-{timestamp}.excalidraw" - -standalone: true -web_bundle: false diff --git a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md b/src/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md deleted file mode 100644 index 3e2b26f41..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md +++ /dev/null @@ -1,38 +0,0 @@ -# Create Wireframe - Validation Checklist - -## Layout Structure - -- [ ] Screen dimensions appropriate for device type -- [ ] Grid alignment (20px) maintained -- [ ] Consistent spacing between UI elements -- [ ] Proper hierarchy (header, content, footer) - -## UI Elements - -- [ ] All interactive elements clearly marked -- [ ] Buttons, inputs, and controls properly sized -- [ ] Text labels readable and appropriately sized -- [ ] Navigation elements clearly indicated - -## Fidelity - -- [ ] Matches requested fidelity level (low/medium/high) -- [ ] Appropriate level of detail -- [ ] Placeholder content used where needed -- [ ] No unnecessary decoration for low-fidelity - -## Annotations - -- [ ] Key interactions annotated -- [ ] Flow indicators present if multi-screen -- [ ] Important notes included -- [ ] Element purposes clear - -## Technical Quality - -- [ ] All elements properly grouped -- [ ] Text elements have containerId -- [ ] Snapped to grid -- [ ] No elements with `isDeleted: true` -- [ ] JSON is valid -- [ ] File saved to correct location diff --git a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md b/src/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md deleted file mode 100644 index dc9506b0d..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md +++ /dev/null @@ -1,133 +0,0 @@ -# Create Wireframe - Workflow Instructions - -```xml -The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml -You MUST have already loaded and processed: {installed_path}/workflow.yaml -This workflow creates website or app wireframes in Excalidraw format. - - - - - Review user's request and extract: wireframe type, fidelity level, screen count, device type, save location - Skip to Step 5 - - - - Ask: "What type of wireframe do you need?" - Present options: - 1. Website (Desktop) - 2. Mobile App (iOS/Android) - 3. Web App (Responsive) - 4. Tablet App - 5. Multi-platform - - WAIT for selection - - - - Ask fidelity level (Low/Medium/High) - Ask screen count (Single/Few 2-3/Multiple 4-6/Many 7+) - Ask device dimensions or use standard - Ask save location - - - - Check for existing theme.json, ask to use if exists - - - - Ask: "Choose a wireframe style:" - Present numbered options: - 1. Classic Wireframe - - Background: #ffffff (white) - - Container: #f5f5f5 (light gray) - - Border: #9e9e9e (gray) - - Text: #424242 (dark gray) - - 2. High Contrast - - Background: #ffffff (white) - - Container: #eeeeee (light gray) - - Border: #212121 (black) - - Text: #000000 (black) - - 3. Blueprint Style - - Background: #1a237e (dark blue) - - Container: #3949ab (blue) - - Border: #7986cb (light blue) - - Text: #ffffff (white) - - 4. Custom - Define your own colors - - WAIT for selection - Create theme.json based on selection - Confirm with user - - - - List all screens and their purposes - Map navigation flow between screens - Identify key UI elements for each screen - Show planned structure, confirm with user - - - - Load {{templates}} and extract `wireframe` section - Load {{library}} - Load theme.json - Load {{helpers}} - - - - Follow {{helpers}} for proper element creation - - For Each Screen: - - Create container/frame - - Add header section - - Add content areas - - Add navigation elements - - Add interactive elements (buttons, inputs) - - Add labels and annotations - - - Build Order: - 1. Screen containers - 2. Layout sections (header, content, footer) - 3. Navigation elements - 4. Content blocks - 5. Interactive elements - 6. Labels and annotations - 7. Flow indicators (if multi-screen) - - - Fidelity Guidelines: - - Low: Basic shapes, minimal detail, placeholder text - - Medium: More defined elements, some styling, representative content - - High: Detailed elements, realistic sizing, actual content examples - - - - - Strip unused elements and elements with isDeleted: true - Save to {{default_output_file}} - - - - NEVER delete the file if validation fails - always fix syntax errors - Run: node -e "JSON.parse(require('fs').readFileSync('{{default_output_file}}', 'utf8')); console.log('✓ Valid JSON')" - - Read the error message carefully - it shows the syntax error and position - Open the file and navigate to the error location - Fix the syntax error (add missing comma, bracket, or quote as indicated) - Save the file - Re-run validation with the same command - Repeat until validation passes - - Once validation passes, confirm with user - - - - Validate against {{validation}} - - - -``` diff --git a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml b/src/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml deleted file mode 100644 index d89005a75..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: create-excalidraw-wireframe -description: "Create website or app wireframes in Excalidraw format" -author: "BMad" - -# Config values -config_source: "{project-root}/_bmad/bmm/config.yaml" -output_folder: "{config_source}:output_folder" - -# Workflow components -installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-wireframe" -shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared" -instructions: "{installed_path}/instructions.md" -validation: "{installed_path}/checklist.md" - -# Core Excalidraw resources (universal knowledge) -helpers: "{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md" -json_validation: "{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md" - -# Domain-specific resources (technical diagrams) -templates: "{shared_path}/excalidraw-templates.yaml" -library: "{shared_path}/excalidraw-library.json" - -# Output file (respects user's configured output_folder) -default_output_file: "{output_folder}/excalidraw-diagrams/wireframe-{timestamp}.excalidraw" - -standalone: true -web_bundle: false diff --git a/src/bmm/workflows/qa/automate/workflow.yaml b/src/bmm/workflows/qa/automate/workflow.yaml index b08727462..847365d7b 100644 --- a/src/bmm/workflows/qa/automate/workflow.yaml +++ b/src/bmm/workflows/qa/automate/workflow.yaml @@ -45,5 +45,3 @@ execution_hints: interactive: false autonomous: true iterative: false - -web_bundle: false diff --git a/src/core/resources/excalidraw/README.md b/src/core/resources/excalidraw/README.md deleted file mode 100644 index c3840bea3..000000000 --- a/src/core/resources/excalidraw/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# Core Excalidraw Resources - -Universal knowledge for creating Excalidraw diagrams. All agents that create Excalidraw files should reference these resources. - -## Purpose - -Provides the **HOW** (universal knowledge) while agents provide the **WHAT** (domain-specific application). - -**Core = "How to create Excalidraw elements"** - -- How to group shapes with text labels -- How to calculate text width -- How to create arrows with proper bindings -- How to validate JSON syntax -- Base structure and primitives - -**Agents = "What diagrams to create"** - -- Frame Expert (BMM): Technical flowcharts, architecture diagrams, wireframes -- Presentation Master (CIS): Pitch decks, creative visuals, Rube Goldberg machines -- Tech Writer (BMM): Documentation diagrams, concept explanations - -## Files in This Directory - -### excalidraw-helpers.md - -**Universal element creation patterns** - -- Text width calculation -- Element grouping rules (shapes + labels) -- Grid alignment -- Arrow creation (straight, elbow) -- Theme application -- Validation checklist -- Optimization rules - -**Agents reference this to:** - -- Create properly grouped shapes -- Calculate text dimensions -- Connect elements with arrows -- Ensure valid structure - -### validate-json-instructions.md - -**Universal JSON validation process** - -- How to validate Excalidraw JSON -- Common errors and fixes -- Workflow integration -- Error recovery - -**Agents reference this to:** - -- Validate files after creation -- Fix syntax errors -- Ensure files can be opened in Excalidraw - -### library-loader.md (Future) - -**How to load external .excalidrawlib files** - -- Programmatic library loading -- Community library integration -- Custom library management - -**Status:** To be developed when implementing external library support. - -## How Agents Use These Resources - -### Example: Frame Expert (Technical Diagrams) - -```yaml -# workflows/excalidraw-diagrams/create-flowchart/workflow.yaml -helpers: '{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md' -json_validation: '{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md' -``` - -**Domain-specific additions:** - -```yaml -# workflows/excalidraw-diagrams/_shared/flowchart-templates.yaml -flowchart: - start_node: - type: ellipse - width: 120 - height: 60 - process_box: - type: rectangle - width: 160 - height: 80 - decision_diamond: - type: diamond - width: 140 - height: 100 -``` - -### Example: Presentation Master (Creative Visuals) - -```yaml -# workflows/create-visual-metaphor/workflow.yaml -helpers: '{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md' -json_validation: '{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md' -``` - -**Domain-specific additions:** - -```yaml -# workflows/_shared/creative-templates.yaml -rube_goldberg: - whimsical_connector: - type: arrow - strokeStyle: dashed - roughness: 2 - playful_box: - type: rectangle - roundness: 12 -``` - -## What Doesn't Belong in Core - -**Domain-Specific Elements:** - -- Flowchart-specific templates (belongs in Frame Expert) -- Pitch deck layouts (belongs in Presentation Master) -- Documentation-specific styles (belongs in Tech Writer) - -**Agent Workflows:** - -- How to create a flowchart (Frame Expert workflow) -- How to create a pitch deck (Presentation Master workflow) -- Step-by-step diagram creation (agent-specific) - -**Theming:** - -- Currently in agent workflows -- **Future:** Will be refactored to core as user-configurable themes - -## Architecture Principle - -**Single Source of Truth:** - -- Core holds universal knowledge -- Agents reference core, don't duplicate -- Updates to core benefit all agents -- Agents specialize with domain knowledge - -**DRY (Don't Repeat Yourself):** - -- Element creation logic: ONCE in core -- Text width calculation: ONCE in core -- Validation process: ONCE in core -- Arrow binding patterns: ONCE in core - -## Future Enhancements - -1. **External Library Loader** - Load .excalidrawlib files from libraries.excalidraw.com -2. **Theme Management** - User-configurable color themes saved in core -3. **Component Library** - Shared reusable components across agents -4. **Layout Algorithms** - Auto-layout helpers for positioning elements diff --git a/src/core/resources/excalidraw/excalidraw-helpers.md b/src/core/resources/excalidraw/excalidraw-helpers.md deleted file mode 100644 index 362646800..000000000 --- a/src/core/resources/excalidraw/excalidraw-helpers.md +++ /dev/null @@ -1,127 +0,0 @@ -# Excalidraw Element Creation Guidelines - -## Text Width Calculation - -For text elements inside shapes (labels): - -``` -text_width = (text.length × fontSize × 0.6) + 20 -``` - -Round to nearest 10 for grid alignment. - -## Element Grouping Rules - -**CRITICAL:** When creating shapes with labels: - -1. Generate unique IDs: - - `shape-id` for the shape - - `text-id` for the text - - `group-id` for the group - -2. Shape element must have: - - `groupIds: [group-id]` - - `boundElements: [{type: "text", id: text-id}]` - -3. Text element must have: - - `containerId: shape-id` - - `groupIds: [group-id]` (SAME as shape) - - `textAlign: "center"` - - `verticalAlign: "middle"` - - `width: calculated_width` - -## Grid Alignment - -- Snap all `x`, `y` coordinates to 20px grid -- Formula: `Math.round(value / 20) * 20` -- Spacing between elements: 60px minimum - -## Arrow Creation - -### Straight Arrows - -Use for forward flow (left-to-right, top-to-bottom): - -```json -{ - "type": "arrow", - "startBinding": { - "elementId": "source-shape-id", - "focus": 0, - "gap": 10 - }, - "endBinding": { - "elementId": "target-shape-id", - "focus": 0, - "gap": 10 - }, - "points": [[0, 0], [distance_x, distance_y]] -} -``` - -### Elbow Arrows - -Use for upward flow, backward flow, or complex routing: - -```json -{ - "type": "arrow", - "startBinding": {...}, - "endBinding": {...}, - "points": [ - [0, 0], - [intermediate_x, 0], - [intermediate_x, intermediate_y], - [final_x, final_y] - ], - "elbowed": true -} -``` - -### Update Connected Shapes - -After creating arrow, update `boundElements` on both connected shapes: - -```json -{ - "id": "shape-id", - "boundElements": [ - { "type": "text", "id": "text-id" }, - { "type": "arrow", "id": "arrow-id" } - ] -} -``` - -## Theme Application - -Theme colors should be applied consistently: - -- **Shapes**: `backgroundColor` from theme primary fill -- **Borders**: `strokeColor` from theme accent -- **Text**: `strokeColor` = "#1e1e1e" (dark text) -- **Arrows**: `strokeColor` from theme accent - -## Validation Checklist - -Before saving, verify: - -- [ ] All shapes with labels have matching `groupIds` -- [ ] All text elements have `containerId` pointing to parent shape -- [ ] Text width calculated properly (no cutoff) -- [ ] Text alignment set (`textAlign` + `verticalAlign`) -- [ ] All elements snapped to 20px grid -- [ ] All arrows have `startBinding` and `endBinding` -- [ ] `boundElements` array updated on connected shapes -- [ ] Theme colors applied consistently -- [ ] No metadata or history in final output -- [ ] All IDs are unique - -## Optimization - -Remove from final output: - -- `appState` object -- `files` object (unless images used) -- All elements with `isDeleted: true` -- Unused library items -- Version history diff --git a/src/core/resources/excalidraw/library-loader.md b/src/core/resources/excalidraw/library-loader.md deleted file mode 100644 index 6fe5ea070..000000000 --- a/src/core/resources/excalidraw/library-loader.md +++ /dev/null @@ -1,50 +0,0 @@ -# External Library Loader - -**Status:** Placeholder for future implementation - -## Purpose - -Load external .excalidrawlib files from or custom sources. - -## Planned Capabilities - -- Load libraries by URL -- Load libraries from local files -- Merge multiple libraries -- Filter library components -- Cache loaded libraries - -## API Reference - -Will document how to use: - -- `importLibrary(url)` - Load library from URL -- `loadSceneOrLibraryFromBlob()` - Load from file -- `mergeLibraryItems()` - Combine libraries - -## Usage Example - -```yaml -# Future workflow.yaml structure -libraries: - - url: 'https://libraries.excalidraw.com/libraries/...' - filter: ['aws', 'cloud'] - - path: '{project-root}/_data/custom-library.excalidrawlib' -``` - -## Implementation Notes - -This will be developed when agents need to leverage the extensive library ecosystem available at . - -Hundreds of pre-built component libraries exist for: - -- AWS/Cloud icons -- UI/UX components -- Business diagrams -- Mind map shapes -- Floor plans -- And much more... - -## User Configuration - -Future: Users will be able to configure favorite libraries in their BMAD config for automatic loading. diff --git a/src/core/resources/excalidraw/validate-json-instructions.md b/src/core/resources/excalidraw/validate-json-instructions.md deleted file mode 100644 index 3abf3fc36..000000000 --- a/src/core/resources/excalidraw/validate-json-instructions.md +++ /dev/null @@ -1,79 +0,0 @@ -# JSON Validation Instructions - -## Purpose - -Validate Excalidraw JSON files after saving to catch syntax errors (missing commas, brackets, quotes). - -## How to Validate - -Use Node.js built-in JSON parsing to validate the file: - -```bash -node -e "JSON.parse(require('fs').readFileSync('FILE_PATH', 'utf8')); console.log('✓ Valid JSON')" -``` - -Replace `FILE_PATH` with the actual file path. - -## Exit Codes - -- Exit code 0 = Valid JSON -- Exit code 1 = Invalid JSON (syntax error) - -## Error Output - -If invalid, Node.js will output: - -- Error message with description -- Position in file where error occurred -- Line and column information (if available) - -## Common Errors and Fixes - -### Missing Comma - -``` -SyntaxError: Expected ',' or '}' after property value -``` - -**Fix:** Add comma after the property value - -### Missing Bracket/Brace - -``` -SyntaxError: Unexpected end of JSON input -``` - -**Fix:** Add missing closing bracket `]` or brace `}` - -### Extra Comma (Trailing) - -``` -SyntaxError: Unexpected token , -``` - -**Fix:** Remove the trailing comma before `]` or `}` - -### Missing Quote - -``` -SyntaxError: Unexpected token -``` - -**Fix:** Add missing quote around string value - -## Workflow Integration - -After saving an Excalidraw file, run validation: - -1. Save the file -2. Run: `node -e "JSON.parse(require('fs').readFileSync('{{save_location}}', 'utf8')); console.log('✓ Valid JSON')"` -3. If validation fails: - - Read the error message for line/position - - Open the file at that location - - Fix the syntax error - - Save and re-validate -4. Repeat until validation passes - -## Critical Rule - -**NEVER delete the file due to validation errors - always fix the syntax error at the reported location.** diff --git a/src/core/tasks/editorial-review-prose.xml b/src/core/tasks/editorial-review-prose.xml index 7ef28f904..deb53570e 100644 --- a/src/core/tasks/editorial-review-prose.xml +++ b/src/core/tasks/editorial-review-prose.xml @@ -1,7 +1,6 @@ + description="Clinical copy-editor that reviews text for communication issues"> Review text for communication issues that impede comprehension and output suggested fixes in a three-column table @@ -10,7 +9,7 @@ + is the final authority on tone, structure, and language choices." /> @@ -62,7 +61,8 @@ - Consult style_guide now and note its key requirements—these override default principles for this review + Consult style_guide now and note its key requirements—these override default principles for this + review Review all prose sections (skip code blocks, frontmatter, structural markup) Identify communication issues that impede comprehension For each issue, determine the minimal fix that achieves clarity @@ -77,16 +77,18 @@ Output: "No editorial issues identified" -| Original Text | Revised Text | Changes | -|---------------|--------------|---------| -| The exact original passage | The suggested revision | Brief explanation of what changed and why | + | Original Text | Revised Text | Changes | + |---------------|--------------|---------| + | The exact original passage | The suggested revision | Brief explanation of what changed and why | -| Original Text | Revised Text | Changes | -|---------------|--------------|---------| -| The system will processes data and it handles errors. | The system processes data and handles errors. | Fixed subject-verb agreement ("will processes" to "processes"); removed redundant "it" | -| Users can chose from options (lines 12, 45, 78) | Users can choose from options | Fixed spelling: "chose" to "choose" (appears in 3 locations) | + | Original Text | Revised Text | Changes | + |---------------|--------------|---------| + | The system will processes data and it handles errors. | The system processes data and handles errors. | Fixed subject-verb + agreement ("will processes" to "processes"); removed redundant "it" | + | Users can chose from options (lines 12, 45, 78) | Users can choose from options | Fixed spelling: "chose" to "choose" (appears in + 3 locations) | @@ -97,4 +99,4 @@ If no issues found after thorough review, output "No editorial issues identified" (this is valid completion, not an error) - + \ No newline at end of file diff --git a/src/core/tasks/editorial-review-structure.xml b/src/core/tasks/editorial-review-structure.xml index aac169ee1..426dc3c8c 100644 --- a/src/core/tasks/editorial-review-structure.xml +++ b/src/core/tasks/editorial-review-structure.xml @@ -4,29 +4,28 @@ + and simplification while preserving comprehension"> Review document structure and propose substantive changes to improve clarity and flow-run this BEFORE copy editing + desc="Document to review (markdown, plain text, or structured content)" /> + is the final authority on tone, structure, and language choices." /> + 'API reference', 'conceptual overview')" /> + 'decision makers')" /> + 'llm' optimizes for precision and density" /> + 'no limit')" /> MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER @@ -69,7 +68,7 @@ Cut emotional language, encouragement, and orientation sections IF concept is well-known from training (e.g., "conventional - commits", "REST APIs"): Reference the standard-don't re-teach it + commits", "REST APIs"): Reference the standard-don't re-teach it ELSE: Be explicit-don't assume the LLM will infer correctly Use consistent terminology-same word for same concept throughout @@ -132,7 +131,8 @@ Note reader_type and which principles apply (human-reader-principles or llm-reader-principles) - Consult style_guide now and note its key requirements—these override default principles for this analysis + Consult style_guide now and note its key requirements—these override default principles for this + analysis Map the document structure: list each major section with its word count Evaluate structure against the selected model's primary rules (e.g., 'Does recommendation come first?' for Pyramid) @@ -176,27 +176,27 @@ Output estimated total reduction if all recommendations accepted Output: "No substantive changes recommended-document structure is sound" -## Document Summary -- **Purpose:** [inferred or provided purpose] -- **Audience:** [inferred or provided audience] -- **Reader type:** [selected reader type] -- **Structure model:** [selected structure model] -- **Current length:** [X] words across [Y] sections + ## Document Summary + - **Purpose:** [inferred or provided purpose] + - **Audience:** [inferred or provided audience] + - **Reader type:** [selected reader type] + - **Structure model:** [selected structure model] + - **Current length:** [X] words across [Y] sections -## Recommendations + ## Recommendations -### 1. [CUT/MERGE/MOVE/CONDENSE/QUESTION/PRESERVE] - [Section or element name] -**Rationale:** [One sentence explanation] -**Impact:** ~[X] words -**Comprehension note:** [If applicable, note impact on reader understanding] + ### 1. [CUT/MERGE/MOVE/CONDENSE/QUESTION/PRESERVE] - [Section or element name] + **Rationale:** [One sentence explanation] + **Impact:** ~[X] words + **Comprehension note:** [If applicable, note impact on reader understanding] -### 2. ... + ### 2. ... -## Summary -- **Total recommendations:** [N] -- **Estimated reduction:** [X] words ([Y]% of original) -- **Meets length target:** [Yes/No/No target specified] -- **Comprehension trade-offs:** [Note any cuts that sacrifice reader engagement for brevity] + ## Summary + - **Total recommendations:** [N] + - **Estimated reduction:** [X] words ([Y]% of original) + - **Meets length target:** [Yes/No/No target specified] + - **Comprehension trade-offs:** [Note any cuts that sacrifice reader engagement for brevity] @@ -206,4 +206,4 @@ If no structural issues found, output "No substantive changes recommended" (this is valid completion, not an error) - + \ No newline at end of file diff --git a/src/core/tasks/help.md b/src/core/tasks/help.md index 3df95fd56..c3c3fab11 100644 --- a/src/core/tasks/help.md +++ b/src/core/tasks/help.md @@ -1,12 +1,11 @@ --- name: help description: Get unstuck by showing what workflow steps come next or answering questions about what to do -standalone: true --- # Task: BMAD Help -## KEY RULES +## ROUTING RULES - **Empty `phase` = anytime** — Universal tools work regardless of workflow state - **Numbered phases indicate sequence** — Phases like `1-discover` → `2-define` → `3-build` → `4-ship` flow in order (naming varies by module) @@ -15,6 +14,26 @@ standalone: true - **`required=true` blocks progress** — Required workflows must complete before proceeding to later phases - **Artifacts reveal completion** — Search resolved output paths for `outputs` patterns, fuzzy-match found files to workflow rows +## DISPLAY RULES + +### Command-Based Workflows +When `command` field has a value: +- Show the command prefixed with `/` (e.g., `/bmad-bmm-create-prd`) + +### Agent-Based Workflows +When `command` field is empty: +- User loads agent first via `/agent-command` +- Then invokes by referencing the `code` field or describing the `name` field +- Do NOT show a slash command — show the code value and agent load instruction instead + +Example presentation for empty command: +``` +Explain Concept (EC) +Load: /tech-writer, then ask to "EC about [topic]" +Agent: Tech Writer +Description: Create clear technical explanations with examples... +``` + ## MODULE DETECTION - **Empty `module` column** → universal tools (work across all modules) @@ -25,38 +44,42 @@ Detect the active module from conversation context, recent workflows, or user qu ## INPUT ANALYSIS Determine what was just completed: -- Did someone state they completed something? Proceed as if that was the input. -- Was a workflow just completed in this conversation? Proceed as if that was the input. -- Search resolved artifact locations for files; fuzzy-match to workflow `outputs` patterns. -- If an `index.md` exists, read it for additional context. +- Explicit completion stated by user +- Workflow completed in current conversation +- Artifacts found matching `outputs` patterns +- If `index.md` exists, read it for additional context - If still unclear, ask: "What workflow did you most recently complete?" ## EXECUTION 1. **Load catalog** — Load `{project-root}/_bmad/_config/bmad-help.csv` -2. **Resolve output locations** — Scan each folder under `_bmad/` (except `_config`) for `config.yaml`. For each workflow row, resolve its `output-location` variables against that module's config so artifact paths can be searched. +2. **Resolve output locations and config** — Scan each folder under `_bmad/` (except `_config`) for `config.yaml`. For each workflow row, resolve its `output-location` variables against that module's config so artifact paths can be searched. Also extract `communication_language` and `project_knowledge` from each scanned module's config. -3. **Analyze input** — Task may provide a workflow name/code, conversational phrase, or nothing. Infer what was just completed using INPUT ANALYSIS above. +3. **Ground in project knowledge** — If `project_knowledge` resolves to an existing path, read available documentation files (architecture docs, project overview, tech stack references) for grounding context. Use discovered project facts when composing any project-specific output. Never fabricate project-specific details — if documentation is unavailable, state so. -4. **Detect active module** — Use MODULE DETECTION above to determine which module the user is working in. +4. **Detect active module** — Use MODULE DETECTION above -5. **Present recommendations** — Show next steps based on completed workflows, phase/sequence ordering (KEY RULES), and artifact detection. Format per the following +5. **Analyze input** — Task may provide a workflow name/code, conversational phrase, or nothing. Infer what was just completed using INPUT ANALYSIS above. -## RECOMMENDED OUTPUT FORMAT +6. **Present recommendations** — Show next steps based on: + - Completed workflows detected + - Phase/sequence ordering (ROUTING RULES) + - Artifact presence **Optional items first** — List optional workflows until a required step is reached **Required items next** — List the next required workflow - For each item show: + + For each item, apply DISPLAY RULES above and include: - Workflow **name** - - **Command** (prefixed with `/`, e.g., `/bmad:example:build-prototype`) + - **Command** OR **Code + Agent load instruction** (per DISPLAY RULES) - **Agent** title and display name from the CSV (e.g., "🎨 Alex (Designer)") - Brief **description** - ### Additional response output guidance to convey: +7. **Additional guidance to convey**: + - Present all output in `{communication_language}` - Run each workflow in a **fresh context window** - - Load the agent using (`/` + `agent-command`), or run the workflow command directly - For **validation workflows**: recommend using a different high-quality LLM if available - For conversational requests: match the user's tone while presenting clearly -6. Return to the calling process after presenting recommendations. +8. Return to the calling process after presenting recommendations. diff --git a/src/core/tasks/index-docs.xml b/src/core/tasks/index-docs.xml index ff9a7de08..30e060921 100644 --- a/src/core/tasks/index-docs.xml +++ b/src/core/tasks/index-docs.xml @@ -1,5 +1,5 @@ + description="Generates or updates an index.md of all documents in the specified directory"> MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER DO NOT skip steps or change the sequence diff --git a/src/core/tasks/review-adversarial-general.xml b/src/core/tasks/review-adversarial-general.xml index 0ebe5cdfa..421719bb5 100644 --- a/src/core/tasks/review-adversarial-general.xml +++ b/src/core/tasks/review-adversarial-general.xml @@ -1,7 +1,7 @@ - + Cynically review content and produce findings @@ -45,4 +45,4 @@ HALT if content is empty or unreadable - + \ No newline at end of file diff --git a/src/core/tasks/shard-doc.xml b/src/core/tasks/shard-doc.xml index cd1dd6748..1dc8fe80e 100644 --- a/src/core/tasks/shard-doc.xml +++ b/src/core/tasks/shard-doc.xml @@ -1,6 +1,5 @@ + description="Splits large markdown documents into smaller, organized files based on level 2 (default) sections"> Split large markdown documents into smaller, organized files based on level 2 sections using @kayvan/markdown-tree-parser tool diff --git a/src/core/tasks/workflow.xml b/src/core/tasks/workflow.xml index fcf6f96be..536c9d8e7 100644 --- a/src/core/tasks/workflow.xml +++ b/src/core/tasks/workflow.xml @@ -1,4 +1,4 @@ - + Execute given workflow by loading its configuration, following instructions, and producing output diff --git a/src/core/workflows/advanced-elicitation/workflow.xml b/src/core/workflows/advanced-elicitation/workflow.xml index 8a348d9ee..ea7395e41 100644 --- a/src/core/workflows/advanced-elicitation/workflow.xml +++ b/src/core/workflows/advanced-elicitation/workflow.xml @@ -1,4 +1,4 @@ - diff --git a/src/core/workflows/party-mode/steps/step-03-graceful-exit.md b/src/core/workflows/party-mode/steps/step-03-graceful-exit.md index eef378774..92274a382 100644 --- a/src/core/workflows/party-mode/steps/step-03-graceful-exit.md +++ b/src/core/workflows/party-mode/steps/step-03-graceful-exit.md @@ -142,6 +142,17 @@ Thank you for using BMAD Party Mode for collaborative multi-agent discussions!" - Express genuine appreciation for user's participation and engagement - Leave user with encouragement for future collaborative sessions +## RETURN PROTOCOL: + +If this workflow was invoked from within a parent workflow: + +1. Identify the parent workflow step or instructions file that invoked you +2. Re-read that file now to restore context +3. Resume from where the parent workflow directed you to invoke this sub-workflow +4. Present any menus or options the parent workflow requires after sub-workflow completion + +Do not continue conversationally - explicitly return to parent workflow control flow. + ## WORKFLOW COMPLETION: After farewell sequence and final closure: diff --git a/tools/build-docs.js b/tools/build-docs.mjs similarity index 74% rename from tools/build-docs.js rename to tools/build-docs.mjs index 38bb379ed..fac767c42 100644 --- a/tools/build-docs.js +++ b/tools/build-docs.mjs @@ -2,26 +2,26 @@ * BMAD Documentation Build Pipeline * * Consolidates docs from multiple sources, generates LLM-friendly files, - * creates downloadable bundles, and builds the Astro+Starlight site. + * and builds the Astro+Starlight site. * * Build outputs: - * build/artifacts/ - With llms.txt, llms-full.txt, ZIPs + * build/artifacts/ - With llms.txt, llms-full.txt * build/site/ - Final Astro output (deployable) */ -const { execSync } = require('node:child_process'); -const fs = require('node:fs'); -const path = require('node:path'); -const archiver = require('archiver'); +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getSiteUrl } from '../website/src/lib/site-url.mjs'; // ============================================================================= // Configuration // ============================================================================= -const PROJECT_ROOT = path.dirname(__dirname); +const PROJECT_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const BUILD_DIR = path.join(PROJECT_ROOT, 'build'); -const SITE_URL = process.env.SITE_URL || 'https://bmad-code-org.github.io/BMAD-METHOD'; const REPO_URL = 'https://github.com/bmad-code-org/BMAD-METHOD'; // DO NOT CHANGE THESE VALUES! @@ -34,10 +34,10 @@ const LLM_EXCLUDE_PATTERNS = [ 'changelog', 'ide-info/', 'v4-to-v6-upgrade', - 'downloads/', 'faq', 'reference/glossary/', 'explanation/game-dev/', + 'bmgd/', // Note: Files/dirs starting with _ (like _STYLE_GUIDE.md, _archive/) are excluded in shouldExcludeFromLlm() ]; @@ -79,17 +79,16 @@ main().catch((error) => { // ============================================================================= // Pipeline Stages /** - * Generate LLM files and downloadable bundles for the documentation pipeline. + * Generate LLM files for the documentation pipeline. * - * Creates the build/artifacts directory, writes `llms.txt` and `llms-full.txt` (sourced from the provided docs directory), - * and produces download ZIP bundles. + * Creates the build/artifacts directory and writes `llms.txt` and `llms-full.txt` (sourced from the provided docs directory). * * @param {string} docsDir - Path to the source docs directory containing Markdown files. * @returns {string} Path to the created artifacts directory. */ async function generateArtifacts(docsDir) { - printHeader('Generating LLM files and download bundles'); + printHeader('Generating LLM files'); const outputDir = path.join(BUILD_DIR, 'artifacts'); fs.mkdirSync(outputDir, { recursive: true }); @@ -97,7 +96,6 @@ async function generateArtifacts(docsDir) { // Generate LLM files reading from docs/, output to artifacts/ generateLlmsTxt(outputDir); generateLlmsFullTxt(docsDir, outputDir); - await generateDownloadBundles(outputDir); console.log(); console.log(` \u001B[32m✓\u001B[0m Artifact generation complete`); @@ -142,39 +140,38 @@ function buildAstroSite() { function generateLlmsTxt(outputDir) { console.log(' → Generating llms.txt...'); + const siteUrl = getSiteUrl(); const content = [ '# BMAD Method Documentation', '', '> AI-driven agile development with specialized agents and workflows that scale from bug fixes to enterprise platforms.', '', - `Documentation: ${SITE_URL}`, + `Documentation: ${siteUrl}`, `Repository: ${REPO_URL}`, - `Full docs: ${SITE_URL}/llms-full.txt`, + `Full docs: ${siteUrl}/llms-full.txt`, '', '## Quick Start', '', - `- **[Quick Start](${SITE_URL}/docs/modules/bmm/quick-start)** - Get started with BMAD Method`, - `- **[Installation](${SITE_URL}/docs/getting-started/installation)** - Installation guide`, + `- **[Quick Start](${siteUrl}/docs/modules/bmm/quick-start)** - Get started with BMAD Method`, + `- **[Installation](${siteUrl}/docs/getting-started/installation)** - Installation guide`, '', '## Core Concepts', '', - `- **[Scale Adaptive System](${SITE_URL}/docs/modules/bmm/scale-adaptive-system)** - Understand BMAD scaling`, - `- **[Quick Flow](${SITE_URL}/docs/modules/bmm/bmad-quick-flow)** - Fast development workflow`, - `- **[Party Mode](${SITE_URL}/docs/modules/bmm/party-mode)** - Multi-agent collaboration`, + `- **[Scale Adaptive System](${siteUrl}/docs/modules/bmm/scale-adaptive-system)** - Understand BMAD scaling`, + `- **[Quick Flow](${siteUrl}/docs/modules/bmm/bmad-quick-flow)** - Fast development workflow`, + `- **[Party Mode](${siteUrl}/docs/modules/bmm/party-mode)** - Multi-agent collaboration`, '', '## Modules', '', - `- **[BMM - Method](${SITE_URL}/docs/modules/bmm/quick-start)** - Core methodology module`, - `- **[BMB - Builder](${SITE_URL}/docs/modules/bmb/)** - Agent and workflow builder`, - `- **[BMGD - Game Dev](${SITE_URL}/docs/modules/bmgd/quick-start)** - Game development module`, + `- **[BMM - Method](${siteUrl}/docs/modules/bmm/quick-start)** - Core methodology module`, + `- **[BMB - Builder](${siteUrl}/docs/modules/bmb/)** - Agent and workflow builder`, + `- **[BMGD - Game Dev](${siteUrl}/docs/modules/bmgd/quick-start)** - Game development module`, '', '---', '', '## Quick Links', '', - `- [Full Documentation (llms-full.txt)](${SITE_URL}/llms-full.txt) - Complete docs for AI context`, - `- [Source Bundle](${SITE_URL}/downloads/bmad-sources.zip) - Complete source code`, - `- [Prompts Bundle](${SITE_URL}/downloads/bmad-prompts.zip) - Agent prompts and workflows`, + `- [Full Documentation (llms-full.txt)](${siteUrl}/llms-full.txt) - Complete docs for AI context`, '', ].join('\n'); @@ -194,7 +191,7 @@ function generateLlmsFullTxt(docsDir, outputDir) { console.log(' → Generating llms-full.txt...'); const date = new Date().toISOString().split('T')[0]; - const files = getAllMarkdownFiles(docsDir); + const files = getAllMarkdownFiles(docsDir).sort(compareLlmDocs); const output = [ '# BMAD Method Documentation (Full)', @@ -236,6 +233,24 @@ function generateLlmsFullTxt(docsDir, outputDir) { ); } +function compareLlmDocs(a, b) { + const aKey = getLlmSortKey(a); + const bKey = getLlmSortKey(b); + + if (aKey !== bKey) return aKey - bKey; + return a.localeCompare(b); +} + +function getLlmSortKey(filePath) { + if (filePath === 'index.md') return 0; + if (filePath.startsWith(`tutorials${path.sep}`) || filePath.startsWith('tutorials/')) return 2; + if (filePath.startsWith(`how-to${path.sep}`) || filePath.startsWith('how-to/')) return 3; + if (filePath.startsWith(`explanation${path.sep}`) || filePath.startsWith('explanation/')) return 4; + if (filePath.startsWith(`reference${path.sep}`) || filePath.startsWith('reference/')) return 5; + if (filePath.startsWith(`bmgd${path.sep}`) || filePath.startsWith('bmgd/')) return 6; + return 7; +} + /** * Collects all Markdown (.md) files under a directory and returns their paths relative to a base directory. * @param {string} dir - Directory to search for Markdown files. @@ -300,48 +315,6 @@ function validateLlmSize(content) { } } -// ============================================================================= -// Download Bundle Generation -// ============================================================================= - -async function generateDownloadBundles(outputDir) { - console.log(' → Generating download bundles...'); - - const downloadsDir = path.join(outputDir, 'downloads'); - fs.mkdirSync(downloadsDir, { recursive: true }); - - await generateSourcesBundle(downloadsDir); - await generatePromptsBundle(downloadsDir); -} - -async function generateSourcesBundle(downloadsDir) { - const srcDir = path.join(PROJECT_ROOT, 'src'); - if (!fs.existsSync(srcDir)) return; - - const zipPath = path.join(downloadsDir, 'bmad-sources.zip'); - await createZipArchive(srcDir, zipPath, ['__pycache__', '.pyc', '.DS_Store', 'node_modules']); - - const size = (fs.statSync(zipPath).size / 1024 / 1024).toFixed(1); - console.log(` bmad-sources.zip (${size}M)`); -} - -/** - * Create a zip archive of the project's prompts modules and place it in the downloads directory. - * - * Creates bmad-prompts.zip from src/modules, excluding common unwanted paths, writes it to the provided downloads directory, and logs the resulting file size. If the modules directory does not exist, the function returns without creating a bundle. - * @param {string} downloadsDir - Destination directory where bmad-prompts.zip will be written. - */ -async function generatePromptsBundle(downloadsDir) { - const modulesDir = path.join(PROJECT_ROOT, 'src', 'modules'); - if (!fs.existsSync(modulesDir)) return; - - const zipPath = path.join(downloadsDir, 'bmad-prompts.zip'); - await createZipArchive(modulesDir, zipPath, ['docs', '.DS_Store', '__pycache__', 'node_modules']); - - const size = Math.floor(fs.statSync(zipPath).size / 1024); - console.log(` bmad-prompts.zip (${size}K)`); -} - // ============================================================================= // Astro Build /** @@ -362,7 +335,6 @@ function runAstroBuild() { * Copy generated artifact files into the built site directory. * * Copies llms.txt and llms-full.txt from the artifacts directory into the site directory. - * If a downloads subdirectory exists under artifacts, copies it into siteDir/downloads. * * @param {string} artifactsDir - Path to the build artifacts directory containing generated files. * @param {string} siteDir - Path to the target site directory where artifacts should be placed. @@ -372,11 +344,6 @@ function copyArtifactsToSite(artifactsDir, siteDir) { fs.copyFileSync(path.join(artifactsDir, 'llms.txt'), path.join(siteDir, 'llms.txt')); fs.copyFileSync(path.join(artifactsDir, 'llms-full.txt'), path.join(siteDir, 'llms-full.txt')); - - const downloadsDir = path.join(artifactsDir, 'downloads'); - if (fs.existsSync(downloadsDir)) { - copyDirectory(downloadsDir, path.join(siteDir, 'downloads')); - } } // ============================================================================= @@ -385,7 +352,7 @@ function copyArtifactsToSite(artifactsDir, siteDir) { * Prints a concise end-of-build summary and displays a sample listing of the final site directory. * * @param {string} docsDir - Path to the source documentation directory used for the build. - * @param {string} artifactsDir - Path to the directory containing generated artifacts (e.g., llms.txt, downloads). + * @param {string} artifactsDir - Path to the directory containing generated artifacts (e.g., llms.txt). * @param {string} siteDir - Path to the final built site directory whose contents will be listed. */ @@ -504,35 +471,6 @@ function copyDirectory(src, dest, exclude = []) { return true; } -/** - * Create a ZIP archive of a directory, optionally excluding entries that match given substrings. - * @param {string} sourceDir - Path to the source directory to archive. - * @param {string} outputPath - Path to write the resulting ZIP file. - * @param {string[]} [exclude=[]] - Array of substrings; any entry whose path includes one of these substrings will be omitted. - * @returns {Promise} Resolves when the archive has been fully written and closed, rejects on error. - */ -function createZipArchive(sourceDir, outputPath, exclude = []) { - return new Promise((resolve, reject) => { - const output = fs.createWriteStream(outputPath); - const archive = archiver('zip', { zlib: { level: 9 } }); - - output.on('close', resolve); - archive.on('error', reject); - - archive.pipe(output); - - const baseName = path.basename(sourceDir); - archive.directory(sourceDir, baseName, (entry) => { - for (const pattern of exclude) { - if (entry.name.includes(pattern)) return false; - } - return entry; - }); - - archive.finalize(); - }); -} - // ============================================================================= // Console Output Formatting // ============================================================================= diff --git a/tools/cli/bmad-cli.js b/tools/cli/bmad-cli.js index ad3aac341..bcd599293 100755 --- a/tools/cli/bmad-cli.js +++ b/tools/cli/bmad-cli.js @@ -1,6 +1,56 @@ const { program } = require('commander'); const path = require('node:path'); const fs = require('node:fs'); +const { execSync } = require('node:child_process'); +const prompts = require('./lib/prompts'); + +// The installer flow uses many sequential @clack/prompts, each adding keypress +// listeners to stdin. Raise the limit to avoid spurious EventEmitter warnings. +if (process.stdin?.setMaxListeners) { + const currentLimit = process.stdin.getMaxListeners(); + process.stdin.setMaxListeners(Math.max(currentLimit, 50)); +} + +// Check for updates - do this asynchronously so it doesn't block startup +const packageJson = require('../../package.json'); +const packageName = 'bmad-method'; +checkForUpdate().catch(() => { + // Silently ignore errors - version check is best-effort +}); + +async function checkForUpdate() { + try { + // For beta versions, check the beta tag; otherwise check latest + const isBeta = + packageJson.version.includes('Beta') || + packageJson.version.includes('beta') || + packageJson.version.includes('alpha') || + packageJson.version.includes('rc'); + const tag = isBeta ? 'beta' : 'latest'; + + const result = execSync(`npm view ${packageName}@${tag} version`, { + encoding: 'utf8', + stdio: 'pipe', + timeout: 5000, + }).trim(); + + if (result && result !== packageJson.version) { + const color = await prompts.getColor(); + const updateMsg = [ + `You are using version ${packageJson.version} but ${result} is available.`, + '', + 'To update, exit and first run:', + ` npm cache clean --force && npx bmad-method@${tag} install`, + ].join('\n'); + await prompts.box(updateMsg, 'Update Available', { + rounded: true, + formatBorder: color.yellow, + }); + } + } catch { + // Silently fail - network issues or npm not available + } +} // Fix for stdin issues when running through npm on Windows // Ensures keyboard interaction works properly with CLI prompts @@ -20,9 +70,6 @@ if (process.stdin.isTTY) { } } -// Load package.json from root for version info -const packageJson = require('../../package.json'); - // Load all command modules const commandsPath = path.join(__dirname, 'commands'); const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith('.js')); diff --git a/tools/cli/commands/install.js b/tools/cli/commands/install.js index ede133a82..961a1a9fa 100644 --- a/tools/cli/commands/install.js +++ b/tools/cli/commands/install.js @@ -1,5 +1,5 @@ -const chalk = require('chalk'); const path = require('node:path'); +const prompts = require('../lib/prompts'); const { Installer } = require('../installers/lib/core/installer'); const { UI } = require('../lib/ui'); @@ -9,20 +9,35 @@ const ui = new UI(); module.exports = { command: 'install', description: 'Install BMAD Core agents and tools', - options: [['-d, --debug', 'Enable debug output for manifest generation']], + options: [ + ['-d, --debug', 'Enable debug output for manifest generation'], + ['--directory ', 'Installation directory (default: current directory)'], + ['--modules ', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'], + [ + '--tools ', + 'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.', + ], + ['--custom-content ', 'Comma-separated list of paths to custom modules/agents/workflows'], + ['--action ', 'Action type for existing installations: install, update, quick-update, or compile-agents'], + ['--user-name ', 'Name for agents to use (default: system username)'], + ['--communication-language ', 'Language for agent communication (default: English)'], + ['--document-output-language ', 'Language for document output (default: English)'], + ['--output-folder ', 'Output folder path relative to project root (default: _bmad-output)'], + ['-y, --yes', 'Accept all defaults and skip prompts where possible'], + ], action: async (options) => { try { // Set debug flag as environment variable for all components if (options.debug) { process.env.BMAD_DEBUG_MANIFEST = 'true'; - console.log(chalk.cyan('Debug mode enabled\n')); + await prompts.log.info('Debug mode enabled'); } - const config = await ui.promptInstall(); + const config = await ui.promptInstall(options); // Handle cancel if (config.actionType === 'cancel') { - console.log(chalk.yellow('Installation cancelled.')); + await prompts.log.warn('Installation cancelled.'); process.exit(0); return; } @@ -30,13 +45,13 @@ module.exports = { // Handle quick update separately if (config.actionType === 'quick-update') { const result = await installer.quickUpdate(config); - console.log(chalk.green('\n✨ Quick update complete!')); - console.log(chalk.cyan(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`)); + await prompts.log.success('Quick update complete!'); + await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`); // Display version-specific end message const { MessageLoader } = require('../installers/lib/message-loader'); const messageLoader = new MessageLoader(); - messageLoader.displayEndMessage(); + await messageLoader.displayEndMessage(); process.exit(0); return; @@ -45,8 +60,8 @@ module.exports = { // Handle compile agents separately if (config.actionType === 'compile-agents') { const result = await installer.compileAgents(config); - console.log(chalk.green('\n✨ Agent recompilation complete!')); - console.log(chalk.cyan(`Recompiled ${result.agentCount} agents with customizations applied`)); + await prompts.log.success('Agent recompilation complete!'); + await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`); process.exit(0); return; } @@ -65,21 +80,22 @@ module.exports = { // Display version-specific end message from install-messages.yaml const { MessageLoader } = require('../installers/lib/message-loader'); const messageLoader = new MessageLoader(); - messageLoader.displayEndMessage(); + await messageLoader.displayEndMessage(); process.exit(0); } } catch (error) { - // Check if error has a complete formatted message - if (error.fullMessage) { - console.error(error.fullMessage); - if (error.stack) { - console.error('\n' + chalk.dim(error.stack)); + try { + if (error.fullMessage) { + await prompts.log.error(error.fullMessage); + } else { + await prompts.log.error(`Installation failed: ${error.message}`); } - } else { - // Generic error handling for all other errors - console.error(chalk.red('Installation failed:'), error.message); - console.error(chalk.dim(error.stack)); + if (error.stack) { + await prompts.log.message(error.stack); + } + } catch { + console.error(error.fullMessage || error.message || error); } process.exit(1); } diff --git a/tools/cli/commands/status.js b/tools/cli/commands/status.js index 5df2cfacd..ec931fe46 100644 --- a/tools/cli/commands/status.js +++ b/tools/cli/commands/status.js @@ -1,5 +1,5 @@ -const chalk = require('chalk'); const path = require('node:path'); +const prompts = require('../lib/prompts'); const { Installer } = require('../installers/lib/core/installer'); const { Manifest } = require('../installers/lib/core/manifest'); const { UI } = require('../lib/ui'); @@ -21,9 +21,9 @@ module.exports = { // Check if bmad directory exists const fs = require('fs-extra'); if (!(await fs.pathExists(bmadDir))) { - console.log(chalk.yellow('No BMAD installation found in the current directory.')); - console.log(chalk.dim(`Expected location: ${bmadDir}`)); - console.log(chalk.dim('\nRun "bmad install" to set up a new installation.')); + await prompts.log.warn('No BMAD installation found in the current directory.'); + await prompts.log.message(`Expected location: ${bmadDir}`); + await prompts.log.message('Run "bmad install" to set up a new installation.'); process.exit(0); return; } @@ -32,8 +32,8 @@ module.exports = { const manifestData = await manifest._readRaw(bmadDir); if (!manifestData) { - console.log(chalk.yellow('No BMAD installation manifest found.')); - console.log(chalk.dim('\nRun "bmad install" to set up a new installation.')); + await prompts.log.warn('No BMAD installation manifest found.'); + await prompts.log.message('Run "bmad install" to set up a new installation.'); process.exit(0); return; } @@ -46,7 +46,7 @@ module.exports = { const availableUpdates = await manifest.checkForUpdates(bmadDir); // Display status - ui.displayStatus({ + await ui.displayStatus({ installation, modules, availableUpdates, @@ -55,9 +55,9 @@ module.exports = { process.exit(0); } catch (error) { - console.error(chalk.red('Status check failed:'), error.message); + await prompts.log.error(`Status check failed: ${error.message}`); if (process.env.BMAD_DEBUG) { - console.error(chalk.dim(error.stack)); + await prompts.log.message(error.stack); } process.exit(1); } diff --git a/tools/cli/external-official-modules.yaml b/tools/cli/external-official-modules.yaml index 436dc01df..431ded4a3 100644 --- a/tools/cli/external-official-modules.yaml +++ b/tools/cli/external-official-modules.yaml @@ -16,7 +16,7 @@ modules: url: https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite module-definition: src/module.yaml code: cis - name: "BMad Creative Innovation Suite" + name: "BMad Creative Intelligence Suite" description: "Creative tools for writing, brainstorming, and more" defaultSelected: false type: bmad-org @@ -26,7 +26,7 @@ modules: url: https://github.com/bmad-code-org/bmad-module-game-dev-studio.git module-definition: src/module.yaml code: gds - name: "BMad Game Dev Suite" + name: "BMad Game Dev Studio" description: "Game development agents and workflows" defaultSelected: false type: bmad-org diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index f8b2042a5..1a0f50d29 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -1,7 +1,6 @@ const path = require('node:path'); const fs = require('fs-extra'); const yaml = require('yaml'); -const chalk = require('chalk'); const { getProjectRoot, getModulePath } = require('../../../lib/project-root'); const { CLIUtils } = require('../../../lib/cli-utils'); const prompts = require('../../../lib/prompts'); @@ -136,10 +135,12 @@ class ConfigCollector { * @param {string} projectDir - Target project directory * @param {Object} options - Additional options * @param {Map} options.customModulePaths - Map of module ID to source path for custom modules + * @param {boolean} options.skipPrompts - Skip prompts and use defaults (for --yes flag) */ async collectAllConfigurations(modules, projectDir, options = {}) { // Store custom module paths for use in collectModuleConfig this.customModulePaths = options.customModulePaths || new Map(); + this.skipPrompts = options.skipPrompts || false; await this.loadExistingConfig(projectDir); // Check if core was already collected (e.g., in early collection phase) @@ -258,15 +259,9 @@ class ConfigCollector { // If module has no config keys at all, handle it specially if (hasNoConfig && moduleConfig.subheader) { - // Add blank line for better readability (matches other modules) - console.log(); const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`; - - // Display the module name in color first (matches other modules) - console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); - - // Show the subheader since there's no configuration to ask about - console.log(chalk.dim(` ✓ ${moduleConfig.subheader}`)); + await prompts.log.step(moduleDisplayName); + await prompts.log.message(` \u2713 ${moduleConfig.subheader}`); return false; // No new fields } @@ -320,7 +315,7 @@ class ConfigCollector { } // Show "no config" message for modules with no new questions (that have config keys) - console.log(chalk.dim(` ✓ ${moduleName.toUpperCase()} module already up to date`)); + await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module already up to date`); return false; // No new fields } @@ -348,15 +343,15 @@ class ConfigCollector { if (questions.length > 0) { // Only show header if we actually have questions - CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader); - console.log(); // Line break before questions + await CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader); + await prompts.log.message(''); const promptedAnswers = await prompts.prompt(questions); // Merge prompted answers with static answers Object.assign(allAnswers, promptedAnswers); } else if (newStaticKeys.length > 0) { // Only static fields, no questions - show no config message - console.log(chalk.dim(` ✓ ${moduleName.toUpperCase()} module configuration updated`)); + await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configuration updated`); } // Store all answers for cross-referencing @@ -583,43 +578,58 @@ class ConfigCollector { // If there are questions to ask, prompt for accepting defaults vs customizing if (questions.length > 0) { const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`; - console.log(); - console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); - let customize = true; - if (moduleName !== 'core') { - const customizeAnswer = await prompts.prompt([ - { - type: 'confirm', - name: 'customize', - message: 'Accept Defaults (no to customize)?', - default: true, - }, - ]); - customize = customizeAnswer.customize; - } - if (customize && moduleName !== 'core') { - // Accept defaults - only ask questions that have NO default value - const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === ''); - - if (questionsWithoutDefaults.length > 0) { - console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`)); - const promptedAnswers = await prompts.prompt(questionsWithoutDefaults); - Object.assign(allAnswers, promptedAnswers); - } - - // For questions with defaults that weren't asked, we need to process them with their default values - const questionsWithDefaults = questions.filter((q) => q.default !== undefined && q.default !== null && q.default !== ''); - for (const question of questionsWithDefaults) { - // Skip function defaults - these are dynamic and will be evaluated later - if (typeof question.default === 'function') { - continue; + // Skip prompts mode: use all defaults without asking + if (this.skipPrompts) { + await prompts.log.info(`Using default configuration for ${moduleDisplayName}`); + // Use defaults for all questions + for (const question of questions) { + const hasDefault = question.default !== undefined && question.default !== null && question.default !== ''; + if (hasDefault && typeof question.default !== 'function') { + allAnswers[question.name] = question.default; } - allAnswers[question.name] = question.default; } } else { - const promptedAnswers = await prompts.prompt(questions); - Object.assign(allAnswers, promptedAnswers); + await prompts.log.step(moduleDisplayName); + let customize = true; + if (moduleName === 'core') { + // Core module: no confirm prompt, continues directly + } else { + // Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing) + const customizeAnswer = await prompts.prompt([ + { + type: 'confirm', + name: 'customize', + message: 'Accept Defaults (no to customize)?', + default: true, + }, + ]); + customize = customizeAnswer.customize; + } + + if (customize && moduleName !== 'core') { + // Accept defaults - only ask questions that have NO default value + const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === ''); + + if (questionsWithoutDefaults.length > 0) { + await prompts.log.message(` Asking required questions for ${moduleName.toUpperCase()}...`); + const promptedAnswers = await prompts.prompt(questionsWithoutDefaults); + Object.assign(allAnswers, promptedAnswers); + } + + // For questions with defaults that weren't asked, we need to process them with their default values + const questionsWithDefaults = questions.filter((q) => q.default !== undefined && q.default !== null && q.default !== ''); + for (const question of questionsWithDefaults) { + // Skip function defaults - these are dynamic and will be evaluated later + if (typeof question.default === 'function') { + continue; + } + allAnswers[question.name] = question.default; + } + } else { + const promptedAnswers = await prompts.prompt(questions); + Object.assign(allAnswers, promptedAnswers); + } } } @@ -728,32 +738,15 @@ class ConfigCollector { const hasNoConfig = actualConfigKeys.length === 0; if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) { - // Module explicitly has no configuration - show with special styling - // Add blank line for better readability (matches other modules) - console.log(); - - // Display the module name in color first (matches other modules) - console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); - - // Ask user if they want to accept defaults or customize on the next line - const { customize } = await prompts.prompt([ - { - type: 'confirm', - name: 'customize', - message: 'Accept Defaults (no to customize)?', - default: true, - }, - ]); - - // Show the subheader if available, otherwise show a default message + await prompts.log.step(moduleDisplayName); if (moduleConfig.subheader) { - console.log(chalk.dim(` ✓ ${moduleConfig.subheader}`)); + await prompts.log.message(` \u2713 ${moduleConfig.subheader}`); } else { - console.log(chalk.dim(` ✓ No custom configuration required`)); + await prompts.log.message(` \u2713 No custom configuration required`); } } else { // Module has config but just no questions to ask - console.log(chalk.dim(` ✓ ${moduleName.toUpperCase()} module configured`)); + await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`); } } @@ -962,14 +955,15 @@ class ConfigCollector { } // Add current value indicator for existing configs + const color = await prompts.getColor(); if (existingValue !== null && existingValue !== undefined) { if (typeof existingValue === 'boolean') { - message += chalk.dim(` (current: ${existingValue ? 'true' : 'false'})`); + message += color.dim(` (current: ${existingValue ? 'true' : 'false'})`); } else if (Array.isArray(existingValue)) { - message += chalk.dim(` (current: ${existingValue.join(', ')})`); + message += color.dim(` (current: ${existingValue.join(', ')})`); } else if (questionType !== 'list') { // Show the cleaned value (without {project-root}/) for display - message += chalk.dim(` (current: ${existingValue})`); + message += color.dim(` (current: ${existingValue})`); } } else if (item.example && questionType === 'input') { // Show example for input fields @@ -979,7 +973,7 @@ class ConfigCollector { exampleText = this.replacePlaceholders(exampleText, moduleName, moduleConfig); exampleText = exampleText.replace('{project-root}/', ''); } - message += chalk.dim(` (e.g., ${exampleText})`); + message += color.dim(` (e.g., ${exampleText})`); } // Build the question object diff --git a/tools/cli/installers/lib/core/dependency-resolver.js b/tools/cli/installers/lib/core/dependency-resolver.js index 317b07f8d..3fb282c5d 100644 --- a/tools/cli/installers/lib/core/dependency-resolver.js +++ b/tools/cli/installers/lib/core/dependency-resolver.js @@ -1,8 +1,8 @@ const fs = require('fs-extra'); const path = require('node:path'); const glob = require('glob'); -const chalk = require('chalk'); const yaml = require('yaml'); +const prompts = require('../../../lib/prompts'); /** * Dependency Resolver for BMAD modules @@ -24,7 +24,7 @@ class DependencyResolver { */ async resolve(bmadDir, selectedModules = [], options = {}) { if (options.verbose) { - console.log(chalk.cyan('Resolving module dependencies...')); + await prompts.log.info('Resolving module dependencies...'); } // Always include core as base @@ -50,7 +50,7 @@ class DependencyResolver { // Report results (only in verbose mode) if (options.verbose) { - this.reportResults(organizedFiles, selectedModules); + await this.reportResults(organizedFiles, selectedModules); } return { @@ -90,8 +90,12 @@ class DependencyResolver { } } + if (!moduleDir) { + continue; + } + if (!(await fs.pathExists(moduleDir))) { - console.warn(chalk.yellow(`Module directory not found: ${moduleDir}`)); + await prompts.log.warn('Module directory not found: ' + moduleDir); continue; } @@ -146,7 +150,7 @@ class DependencyResolver { const content = await fs.readFile(file.path, 'utf8'); // Parse YAML frontmatter for explicit dependencies - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { try { // Pre-process to handle backticks in YAML values @@ -179,7 +183,7 @@ class DependencyResolver { } } } catch (error) { - console.warn(chalk.yellow(`Failed to parse frontmatter in ${file.name}: ${error.message}`)); + await prompts.log.warn('Failed to parse frontmatter in ' + file.name + ': ' + error.message); } } @@ -658,8 +662,8 @@ class DependencyResolver { /** * Report resolution results */ - reportResults(organized, selectedModules) { - console.log(chalk.green('\n✓ Dependency resolution complete')); + async reportResults(organized, selectedModules) { + await prompts.log.success('Dependency resolution complete'); for (const [module, files] of Object.entries(organized)) { const isSelected = selectedModules.includes(module) || module === 'core'; @@ -667,31 +671,31 @@ class DependencyResolver { files.agents.length + files.tasks.length + files.tools.length + files.templates.length + files.data.length + files.other.length; if (totalFiles > 0) { - console.log(chalk.cyan(`\n ${module.toUpperCase()} module:`)); - console.log(chalk.dim(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`)); + await prompts.log.info(` ${module.toUpperCase()} module:`); + await prompts.log.message(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`); if (files.agents.length > 0) { - console.log(chalk.dim(` Agents: ${files.agents.length}`)); + await prompts.log.message(` Agents: ${files.agents.length}`); } if (files.tasks.length > 0) { - console.log(chalk.dim(` Tasks: ${files.tasks.length}`)); + await prompts.log.message(` Tasks: ${files.tasks.length}`); } if (files.templates.length > 0) { - console.log(chalk.dim(` Templates: ${files.templates.length}`)); + await prompts.log.message(` Templates: ${files.templates.length}`); } if (files.data.length > 0) { - console.log(chalk.dim(` Data files: ${files.data.length}`)); + await prompts.log.message(` Data files: ${files.data.length}`); } if (files.other.length > 0) { - console.log(chalk.dim(` Other files: ${files.other.length}`)); + await prompts.log.message(` Other files: ${files.other.length}`); } } } if (this.missingDependencies.size > 0) { - console.log(chalk.yellow('\n ⚠ Missing dependencies:')); + await prompts.log.warn('Missing dependencies:'); for (const missing of this.missingDependencies) { - console.log(chalk.yellow(` - ${missing}`)); + await prompts.log.warn(` - ${missing}`); } } } diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index a14c3d192..1e161bdc8 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -1,7 +1,5 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); -const ora = require('ora'); const { Detector } = require('./detector'); const { Manifest } = require('./manifest'); const { ModuleManager } = require('../modules/manager'); @@ -17,9 +15,7 @@ const { ManifestGenerator } = require('./manifest-generator'); const { IdeConfigManager } = require('./ide-config-manager'); const { CustomHandler } = require('../custom/handler'); const prompts = require('../../../lib/prompts'); - -// BMAD installation folder name - this is constant and should never change -const BMAD_FOLDER_NAME = '_bmad'; +const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); class Installer { constructor() { @@ -168,32 +164,32 @@ class Installer { const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide)); if (newlySelectedIdes.length > 0) { - console.log('\n'); // Add spacing before IDE questions - // Collect configuration for IDEs that support it for (const ide of newlySelectedIdes) { try { const handler = this.ideManager.handlers.get(ide); if (!handler) { - console.warn(chalk.yellow(`Warning: IDE '${ide}' handler not found`)); + await prompts.log.warn(`Warning: IDE '${ide}' handler not found`); continue; } // Check if this IDE handler has a collectConfiguration method // (custom installers like Codex, Kilo, Kiro-cli may have this) if (typeof handler.collectConfiguration === 'function') { - console.log(chalk.cyan(`\nConfiguring ${ide}...`)); + await prompts.log.info(`Configuring ${ide}...`); ideConfigurations[ide] = await handler.collectConfiguration({ selectedModules: selectedModules || [], projectDir, bmadDir, }); + } else { + // Config-driven IDEs don't need configuration - mark as ready + ideConfigurations[ide] = { _noConfigNeeded: true }; } - // Most config-driven IDEs don't need configuration - silently skip } catch (error) { // IDE doesn't support configuration or has an error - console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}: ${error.message}`)); + await prompts.log.warn(`Warning: Could not load configuration for ${ide}: ${error.message}`); } } } @@ -201,7 +197,7 @@ class Installer { // Log which IDEs are already configured and being kept const keptIdes = toolConfig.ides.filter((ide) => previouslyConfiguredIdes.includes(ide)); if (keptIdes.length > 0) { - console.log(chalk.dim(`\nKeeping existing configuration for: ${keptIdes.join(', ')}`)); + await prompts.log.message(`Keeping existing configuration for: ${keptIdes.join(', ')}`); } } @@ -231,16 +227,17 @@ class Installer { // Only display logo if core config wasn't already collected (meaning we're not continuing from UI) if (!hasCoreConfig) { // Display BMAD logo - CLIUtils.displayLogo(); + await CLIUtils.displayLogo(); // Display welcome message - CLIUtils.displaySection('BMad™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version); + await CLIUtils.displaySection('BMad™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version); } // Note: Legacy V4 detection now happens earlier in UI.promptInstall() // before any config collection, so we don't need to check again here const projectDir = path.resolve(config.directory); + const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME); // If core config was pre-collected (from interactive mode), use it if (config.coreConfig && Object.keys(config.coreConfig).length > 0) { @@ -355,11 +352,13 @@ class Installer { const modulesWithoutCore = allModulesForConfig.filter((m) => m !== 'core'); moduleConfigs = await this.configCollector.collectAllConfigurations(modulesWithoutCore, path.resolve(config.directory), { customModulePaths, + skipPrompts: config.skipPrompts, }); } else { // Core not collected yet, include it moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), { customModulePaths, + skipPrompts: config.skipPrompts, }); } } @@ -372,41 +371,36 @@ class Installer { // Tool selection will be collected after we determine if it's a reinstall/update/new install - const spinner = ora('Preparing installation...').start(); + const spinner = await prompts.spinner(); + spinner.start('Preparing installation...'); try { - // Resolve target directory (path.resolve handles platform differences) - const projectDir = path.resolve(config.directory); - - // Always use the standard _bmad folder name - const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME); - // Create a project directory if it doesn't exist (user already confirmed) if (!(await fs.pathExists(projectDir))) { - spinner.text = 'Creating installation directory...'; + spinner.message('Creating installation directory...'); try { // fs.ensureDir handles platform-specific directory creation // It will recursively create all necessary parent directories await fs.ensureDir(projectDir); } catch (error) { - spinner.fail('Failed to create installation directory'); - console.error(chalk.red(`Error: ${error.message}`)); + spinner.error('Failed to create installation directory'); + await prompts.log.error(`Error: ${error.message}`); // More detailed error for common issues if (error.code === 'EACCES') { - console.error(chalk.red('Permission denied. Check parent directory permissions.')); + await prompts.log.error('Permission denied. Check parent directory permissions.'); } else if (error.code === 'ENOSPC') { - console.error(chalk.red('No space left on device.')); + await prompts.log.error('No space left on device.'); } throw new Error(`Cannot create directory: ${projectDir}`); } } // Check existing installation - spinner.text = 'Checking for existing installation...'; + spinner.message('Checking for existing installation...'); const existingInstall = await this.detector.detect(bmadDir); if (existingInstall.installed && !config.force && !config._quickUpdate) { - spinner.stop(); + spinner.stop('Existing installation detected'); // Check if user already decided what to do (from early menu in ui.js) let action = null; @@ -414,9 +408,9 @@ class Installer { action = 'update'; } else { // Fallback: Ask the user (backwards compatibility for other code paths) - console.log(chalk.yellow('\n⚠️ Existing BMAD installation detected')); - console.log(chalk.dim(` Location: ${bmadDir}`)); - console.log(chalk.dim(` Version: ${existingInstall.version}`)); + await prompts.log.warn('Existing BMAD installation detected'); + await prompts.log.message(` Location: ${bmadDir}`); + await prompts.log.message(` Version: ${existingInstall.version}`); const promptResult = await this.promptUpdateAction(); action = promptResult.action; @@ -438,17 +432,17 @@ class Installer { // If there are modules to remove, ask for confirmation if (modulesToRemove.length > 0) { const prompts = require('../../../lib/prompts'); - spinner.stop(); + if (spinner.isSpinning) { + spinner.stop('Reviewing module changes'); + } - console.log(''); - console.log(chalk.yellow.bold('⚠️ Modules to be removed:')); + await prompts.log.warn('Modules to be removed:'); for (const moduleId of modulesToRemove) { const moduleInfo = existingInstall.modules.find((m) => m.id === moduleId); const displayName = moduleInfo?.name || moduleId; const modulePath = path.join(bmadDir, moduleId); - console.log(chalk.red(` - ${displayName} (${modulePath})`)); + await prompts.log.error(` - ${displayName} (${modulePath})`); } - console.log(''); const confirmRemoval = await prompts.confirm({ message: `Remove ${modulesToRemove.length} module(s) from BMAD installation?`, @@ -462,15 +456,15 @@ class Installer { try { if (await fs.pathExists(modulePath)) { await fs.remove(modulePath); - console.log(chalk.dim(` ✓ Removed: ${moduleId}`)); + await prompts.log.message(` Removed: ${moduleId}`); } } catch (error) { - console.warn(chalk.yellow(` Warning: Failed to remove ${moduleId}: ${error.message}`)); + await prompts.log.warn(` Warning: Failed to remove ${moduleId}: ${error.message}`); } } - console.log(chalk.green(` ✓ Removed ${modulesToRemove.length} module(s)`)); + await prompts.log.success(` Removed ${modulesToRemove.length} module(s)`); } else { - console.log(chalk.dim(' → Module removal cancelled')); + await prompts.log.message(' Module removal cancelled'); // Add the modules back to the selection since user cancelled removal for (const moduleId of modulesToRemove) { if (!config.modules) config.modules = []; @@ -503,7 +497,7 @@ class Installer { // Also store in configCollector for use during config collection this.configCollector.collectedConfig.core = existingCoreConfig; } catch (error) { - console.warn(chalk.yellow(`Warning: Could not read existing core config: ${error.message}`)); + await prompts.log.warn(`Warning: Could not read existing core config: ${error.message}`); } } @@ -554,7 +548,7 @@ class Installer { await fs.ensureDir(path.dirname(backupPath)); await fs.copy(customFile, backupPath); } - spinner.succeed(`Backed up ${customFiles.length} custom files`); + spinner.stop(`Backed up ${customFiles.length} custom files`); config._tempBackupDir = tempBackupDir; } @@ -571,14 +565,14 @@ class Installer { await fs.ensureDir(path.dirname(tempBackupPath)); await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true }); } - spinner.succeed(`Backed up ${modifiedFiles.length} modified files`); + spinner.stop(`Backed up ${modifiedFiles.length} modified files`); config._tempModifiedBackupDir = tempModifiedBackupDir; } } } else if (existingInstall.installed && config._quickUpdate) { // Quick update mode - automatically treat as update without prompting - spinner.text = 'Preparing quick update...'; + spinner.message('Preparing quick update...'); config._isUpdate = true; config._existingInstall = existingInstall; @@ -636,7 +630,7 @@ class Installer { await fs.ensureDir(path.dirname(backupPath)); await fs.copy(customFile, backupPath); } - spinner.succeed(`Backed up ${customFiles.length} custom files`); + spinner.stop(`Backed up ${customFiles.length} custom files`); config._tempBackupDir = tempBackupDir; } @@ -652,14 +646,14 @@ class Installer { await fs.ensureDir(path.dirname(tempBackupPath)); await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true }); } - spinner.succeed(`Backed up ${modifiedFiles.length} modified files`); + spinner.stop(`Backed up ${modifiedFiles.length} modified files`); config._tempModifiedBackupDir = tempModifiedBackupDir; } } // Now collect tool configurations after we know if it's a reinstall // Skip for quick update since we already have the IDE list - spinner.stop(); + spinner.stop('Pre-checks complete'); let toolSelection; if (config._quickUpdate) { // Quick update already has IDEs configured, use saved configurations @@ -682,7 +676,8 @@ class Installer { } else { // Pass pre-selected IDEs from early prompt (if available) // This allows IDE selection to happen before file copying, improving UX - const preSelectedIdes = config.ides && config.ides.length > 0 ? config.ides : null; + // Use config.ides if it's an array (even if empty), null means prompt + const preSelectedIdes = Array.isArray(config.ides) ? config.ides : null; toolSelection = await this.collectToolConfigurations( path.resolve(config.directory), config.modules, @@ -697,22 +692,23 @@ class Installer { config.skipIde = toolSelection.skipIde; const ideConfigurations = toolSelection.configurations; - // Add spacing after prompts before installation progress - console.log(''); + // Results collector for consolidated summary + const results = []; + const addResult = (step, status, detail = '') => results.push({ step, status, detail }); if (spinner.isSpinning) { - spinner.text = 'Continuing installation...'; + spinner.message('Installing...'); } else { - spinner.start('Continuing installation...'); + spinner.start('Installing...'); } // Create bmad directory structure - spinner.text = 'Creating directory structure...'; + spinner.message('Creating directory structure...'); await this.createDirectoryStructure(bmadDir); // Cache custom modules if any if (customModulePaths && customModulePaths.size > 0) { - spinner.text = 'Caching custom modules...'; + spinner.message('Caching custom modules...'); const { CustomModuleCache } = require('./custom-module-cache'); const customCache = new CustomModuleCache(bmadDir); @@ -727,16 +723,16 @@ class Installer { // Update module manager with the cached paths this.moduleManager.setCustomModulePaths(customModulePaths); - spinner.succeed('Custom modules cached'); + addResult('Custom modules cached', 'ok'); } const projectRoot = getProjectRoot(); // Step 1: Install core module first (if requested) if (config.installCore) { - spinner.start('Installing BMAD core...'); + spinner.message('Installing BMAD core...'); await this.installCoreWithDependencies(bmadDir, { core: {} }); - spinner.succeed('Core installed'); + addResult('Core', 'ok', 'installed'); // Generate core config file await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} }); @@ -806,13 +802,13 @@ class Installer { bmadDir: bmadDir, // Pass bmadDir so we can check cache }); + spinner.message('Resolving dependencies...'); + const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, { verbose: config.verbose, moduleManager: tempModuleManager, }); - spinner.succeed('Dependencies resolved'); - // Install modules with their dependencies if (allModules && allModules.length > 0) { const installedModuleNames = new Set(); @@ -826,7 +822,7 @@ class Installer { // Show appropriate message based on whether this is a quick update const isQuickUpdate = config._quickUpdate || false; - spinner.start(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`); + spinner.message(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`); // Check if this is a custom module let isCustomModule = false; @@ -900,6 +896,7 @@ class Installer { moduleConfig: collectedModuleConfig, isQuickUpdate: config._quickUpdate || false, installer: this, + silent: true, }, ); @@ -917,7 +914,7 @@ class Installer { } } - spinner.succeed(`Module ${isQuickUpdate ? 'updated' : 'installed'}: ${moduleName}`); + addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed'); } // Install partial modules (only dependencies) @@ -931,9 +928,8 @@ class Installer { files.data.length + files.other.length; if (totalFiles > 0) { - spinner.start(`Installing ${module} dependencies...`); + spinner.message(`Installing ${module} dependencies...`); await this.installPartialModule(module, bmadDir, files); - spinner.succeed(`${module} dependencies installed`); } } } @@ -942,9 +938,9 @@ class Installer { // All content is now installed as modules - no separate custom content handling needed // Generate clean config.yaml files for each installed module - spinner.start('Generating module configurations...'); + spinner.message('Generating module configurations...'); await this.generateModuleConfigs(bmadDir, moduleConfigs); - spinner.succeed('Module configurations generated'); + addResult('Configurations', 'ok', 'generated'); // Create agent configuration files // Note: Legacy createAgentConfigs removed - using YAML customize system instead @@ -959,7 +955,7 @@ class Installer { // Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup // This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv - spinner.start('Generating workflow and agent manifests...'); + spinner.message('Generating workflow and agent manifests...'); const manifestGen = new ManifestGenerator(); // For quick update, we need ALL installed modules in the manifest @@ -987,15 +983,17 @@ class Installer { // Custom modules are now included in the main modules list - no separate tracking needed - spinner.succeed( - `Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`, + addResult( + 'Manifests', + 'ok', + `${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`, ); // Merge all module-help.csv files into bmad-help.csv // This must happen AFTER generateManifests because it depends on agent-manifest.csv - spinner.start('Generating workflow help catalog...'); + spinner.message('Generating workflow help catalog...'); await this.mergeModuleHelpCatalogs(bmadDir); - spinner.succeed('Workflow help catalog generated'); + addResult('Help catalog', 'ok'); // Configure IDEs and copy documentation if (!config.skipIde && config.ides && config.ides.length > 0) { @@ -1006,64 +1004,63 @@ class Installer { const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string'); if (validIdes.length === 0) { - console.log(chalk.yellow('⚠️ No valid IDEs selected. Skipping IDE configuration.')); + addResult('IDE configuration', 'warn', 'no valid IDEs selected'); } else { // Check if any IDE might need prompting (no pre-collected config) const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]); - if (!needsPrompting) { - spinner.start('Configuring IDEs...'); - } - // Temporarily suppress console output if not verbose const originalLog = console.log; if (!config.verbose) { console.log = () => {}; } - for (const ide of validIdes) { - // Only show spinner if we have pre-collected config (no prompts expected) - if (ideConfigurations[ide] && !needsPrompting) { - spinner.text = `Configuring ${ide}...`; - } else if (!ideConfigurations[ide]) { - // Stop spinner before prompting - if (spinner.isSpinning) { - spinner.stop(); + try { + for (const ide of validIdes) { + if (!needsPrompting || ideConfigurations[ide]) { + // All IDEs pre-configured, or this specific IDE has config: keep spinner running + spinner.message(`Configuring ${ide}...`); + } else { + // This IDE needs prompting: stop spinner to allow user interaction + if (spinner.isSpinning) { + spinner.stop('Ready for IDE configuration'); + } + } + + // Silent when this IDE has pre-collected config (no prompts for THIS IDE) + const ideHasConfig = Boolean(ideConfigurations[ide]); + const setupResult = await this.ideManager.setup(ide, projectDir, bmadDir, { + selectedModules: allModules || [], + preCollectedConfig: ideConfigurations[ide] || null, + verbose: config.verbose, + silent: ideHasConfig, + }); + + // Save IDE configuration for future updates + if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) { + await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); + } + + // Collect result for summary + if (setupResult.success) { + addResult(ide, 'ok', setupResult.detail || ''); + } else { + addResult(ide, 'error', setupResult.error || 'failed'); + } + + // Restart spinner if we stopped it for prompting + if (needsPrompting && !spinner.isSpinning) { + spinner.start('Configuring IDEs...'); } - console.log(chalk.cyan(`\nConfiguring ${ide}...`)); } - - // Pass pre-collected configuration to avoid re-prompting - await this.ideManager.setup(ide, projectDir, bmadDir, { - selectedModules: allModules || [], - preCollectedConfig: ideConfigurations[ide] || null, - verbose: config.verbose, - }); - - // Save IDE configuration for future updates - if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) { - await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); - } - - // Restart spinner if we stopped it - if (!ideConfigurations[ide] && !spinner.isSpinning) { - spinner.start('Configuring IDEs...'); - } - } - - // Restore console.log - console.log = originalLog; - - if (spinner.isSpinning) { - spinner.succeed(`Configured: ${validIdes.join(', ')}`); - } else { - console.log(chalk.green(`✓ Configured: ${validIdes.join(', ')}`)); + } finally { + console.log = originalLog; } } } // Run module-specific installers after IDE setup - spinner.start('Running module-specific installers...'); + spinner.message('Running module-specific installers...'); // Create a conditional logger based on verbose mode const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose; @@ -1075,20 +1072,21 @@ class Installer { // Run core module installer if core was installed if (config.installCore || resolution.byModule.core) { - spinner.text = 'Running core module installer...'; + spinner.message('Running core module installer...'); await this.moduleManager.runModuleInstaller('core', bmadDir, { installedIDEs: config.ides || [], moduleConfig: moduleConfigs.core || {}, coreConfig: moduleConfigs.core || {}, logger: moduleLogger, + silent: true, }); } // Run installers for user-selected modules if (config.modules && config.modules.length > 0) { for (const moduleName of config.modules) { - spinner.text = `Running ${moduleName} module installer...`; + spinner.message(`Running ${moduleName} module installer...`); // Pass installed IDEs and module config to module installer await this.moduleManager.runModuleInstaller(moduleName, bmadDir, { @@ -1096,11 +1094,12 @@ class Installer { moduleConfig: moduleConfigs[moduleName] || {}, coreConfig: moduleConfigs.core || {}, logger: moduleLogger, + silent: true, }); } } - spinner.succeed('Module-specific installers completed'); + addResult('Module installers', 'ok'); // Note: Manifest files are already created by ManifestGenerator above // No need to create legacy manifest.csv anymore @@ -1110,7 +1109,7 @@ class Installer { let modifiedFiles = []; if (config._isUpdate) { if (config._customFiles && config._customFiles.length > 0) { - spinner.start(`Restoring ${config._customFiles.length} custom files...`); + spinner.message(`Restoring ${config._customFiles.length} custom files...`); for (const originalPath of config._customFiles) { const relativePath = path.relative(bmadDir, originalPath); @@ -1127,7 +1126,6 @@ class Installer { await fs.remove(config._tempBackupDir); } - spinner.succeed(`Restored ${config._customFiles.length} custom files`); customFiles = config._customFiles; } @@ -1136,7 +1134,7 @@ class Installer { // Restore modified files as .bak files if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) { - spinner.start(`Restoring ${modifiedFiles.length} modified files as .bak...`); + spinner.message(`Restoring ${modifiedFiles.length} modified files as .bak...`); for (const modifiedFile of modifiedFiles) { const relativePath = path.relative(bmadDir, modifiedFile.path); @@ -1151,37 +1149,20 @@ class Installer { // Clean up temp backup await fs.remove(config._tempModifiedBackupDir); - - spinner.succeed(`Restored ${modifiedFiles.length} modified files as .bak`); } } } - spinner.stop(); + // Stop the single installation spinner + spinner.stop('Installation complete'); - // Report custom and modified files if any were found - if (customFiles.length > 0) { - console.log(chalk.cyan(`\n📁 Custom files preserved: ${customFiles.length}`)); - } - - if (modifiedFiles.length > 0) { - console.log(chalk.yellow(`\n⚠️ User modified files detected: ${modifiedFiles.length}`)); - console.log( - chalk.dim( - '\nThese user modified files have been updated with the new version, search the project for .bak files that had your customizations.', - ), - ); - console.log(chalk.dim('Remove these .bak files it no longer needed\n')); - } - - // Display completion message - const { UI } = require('../../../lib/ui'); - const ui = new UI(); - ui.showInstallSummary({ - path: bmadDir, + // Render consolidated summary + await this.renderInstallSummary(results, { + bmadDir, modules: config.modules, ides: config.ides, customFiles: customFiles.length > 0 ? customFiles : undefined, + modifiedFiles: modifiedFiles.length > 0 ? modifiedFiles : undefined, }); return { @@ -1192,16 +1173,63 @@ class Installer { projectDir: projectDir, }; } catch (error) { - spinner.fail('Installation failed'); + spinner.error('Installation failed'); throw error; } } + /** + * Render a consolidated install summary using prompts.note() + * @param {Array} results - Array of {step, status: 'ok'|'error'|'warn', detail} + * @param {Object} context - {bmadDir, modules, ides, customFiles, modifiedFiles} + */ + async renderInstallSummary(results, context = {}) { + const color = await prompts.getColor(); + + // Build step lines with status indicators + const lines = []; + for (const r of results) { + let icon; + if (r.status === 'ok') { + icon = color.green('\u2713'); + } else if (r.status === 'warn') { + icon = color.yellow('!'); + } else { + icon = color.red('\u2717'); + } + const detail = r.detail ? color.dim(` (${r.detail})`) : ''; + lines.push(` ${icon} ${r.step}${detail}`); + } + + // Add context info + lines.push(''); + if (context.bmadDir) { + lines.push(` Installed to: ${color.dim(context.bmadDir)}`); + } + if (context.modules && context.modules.length > 0) { + lines.push(` Modules: ${color.dim(context.modules.join(', '))}`); + } + if (context.ides && context.ides.length > 0) { + lines.push(` Tools: ${color.dim(context.ides.join(', '))}`); + } + + // Custom/modified file warnings + if (context.customFiles && context.customFiles.length > 0) { + lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`); + } + if (context.modifiedFiles && context.modifiedFiles.length > 0) { + lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`); + } + + await prompts.note(lines.join('\n'), 'BMAD is ready to use!'); + } + /** * Update existing installation */ async update(config) { - const spinner = ora('Checking installation...').start(); + const spinner = await prompts.spinner(); + spinner.start('Checking installation...'); try { const projectDir = path.resolve(config.directory); @@ -1209,11 +1237,11 @@ class Installer { const existingInstall = await this.detector.detect(bmadDir); if (!existingInstall.installed) { - spinner.fail('No BMAD installation found'); + spinner.stop('No BMAD installation found'); throw new Error(`No BMAD installation found at ${bmadDir}`); } - spinner.text = 'Analyzing update requirements...'; + spinner.message('Analyzing update requirements...'); // Compare versions and determine what needs updating const currentVersion = existingInstall.version; @@ -1267,8 +1295,8 @@ class Installer { } if (customModuleSources.size > 0) { - spinner.stop(); - console.log(chalk.yellow('\nChecking custom module sources before update...')); + spinner.stop('Update analysis complete'); + await prompts.log.warn('Checking custom module sources before update...'); const projectRoot = getProjectRoot(); await this.handleMissingCustomSources( @@ -1283,43 +1311,43 @@ class Installer { } if (config.dryRun) { - spinner.stop(); - console.log(chalk.cyan('\n🔍 Update Preview (Dry Run)\n')); - console.log(chalk.bold('Current version:'), currentVersion); - console.log(chalk.bold('New version:'), newVersion); - console.log(chalk.bold('Core:'), existingInstall.hasCore ? 'Will be updated' : 'Not installed'); + spinner.stop('Dry run analysis complete'); + let dryRunContent = `Current version: ${currentVersion}\n`; + dryRunContent += `New version: ${newVersion}\n`; + dryRunContent += `Core: ${existingInstall.hasCore ? 'Will be updated' : 'Not installed'}`; if (existingInstall.modules.length > 0) { - console.log(chalk.bold('\nModules to update:')); + dryRunContent += '\n\nModules to update:'; for (const mod of existingInstall.modules) { - console.log(` - ${mod.id}`); + dryRunContent += `\n - ${mod.id}`; } } + await prompts.note(dryRunContent, 'Update Preview (Dry Run)'); return; } // Perform actual update if (existingInstall.hasCore) { - spinner.text = 'Updating core...'; + spinner.message('Updating core...'); await this.updateCore(bmadDir, config.force); } for (const module of existingInstall.modules) { - spinner.text = `Updating module: ${module.id}...`; - await this.moduleManager.update(module.id, bmadDir, config.force); + spinner.message(`Updating module: ${module.id}...`); + await this.moduleManager.update(module.id, bmadDir, config.force, { installer: this }); } // Update manifest - spinner.text = 'Updating manifest...'; + spinner.message('Updating manifest...'); await this.manifest.update(bmadDir, { version: newVersion, updateDate: new Date().toISOString(), }); - spinner.succeed('Update complete'); + spinner.stop('Update complete'); return { success: true }; } catch (error) { - spinner.fail('Update failed'); + spinner.error('Update failed'); throw error; } } @@ -1494,10 +1522,10 @@ class Installer { } if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Merged module-help from: ${moduleName}`)); + await prompts.log.message(` Merged module-help from: ${moduleName}`); } } catch (error) { - console.warn(chalk.yellow(` Warning: Failed to read module-help.csv from ${moduleName}:`, error.message)); + await prompts.log.warn(` Warning: Failed to read module-help.csv from ${moduleName}: ${error.message}`); } } } @@ -1539,7 +1567,7 @@ class Installer { this.installedFiles.add(outputPath); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Generated bmad-help.csv: ${allRows.length} workflows`)); + await prompts.log.message(` Generated bmad-help.csv: ${allRows.length} workflows`); } } @@ -1730,6 +1758,7 @@ class Installer { skipModuleInstaller: true, // We'll run it later after IDE setup moduleConfig: moduleConfig, // Pass module config for conditional filtering installer: this, + silent: true, }, ); @@ -1909,7 +1938,7 @@ class Installer { // Check for localskip="true" in the agent tag const agentMatch = content.match(/]*\slocalskip="true"[^>]*>/); if (agentMatch) { - console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`)); + await prompts.log.message(` Skipping web-only agent: ${path.basename(file)}`); continue; // Skip this agent } } @@ -1996,7 +2025,7 @@ class Installer { if (await fs.pathExists(genericTemplatePath)) { await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`)); + await prompts.log.message(` Created customize: ${moduleName}-${agentName}.customize.yaml`); } } } @@ -2031,8 +2060,8 @@ class Installer { * @returns {Object} Update result */ async quickUpdate(config) { - const ora = require('ora'); - const spinner = ora('Starting quick update...').start(); + const spinner = await prompts.spinner(); + spinner.start('Starting quick update...'); try { const projectDir = path.resolve(config.directory); @@ -2040,11 +2069,11 @@ class Installer { // Check if bmad directory exists if (!(await fs.pathExists(bmadDir))) { - spinner.fail('No BMAD installation found'); + spinner.stop('No BMAD installation found'); throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`); } - spinner.text = 'Detecting installed modules and configuration...'; + spinner.message('Detecting installed modules and configuration...'); // Detect existing installation const existingInstall = await this.detector.detect(bmadDir); @@ -2171,14 +2200,14 @@ class Installer { } } - spinner.succeed(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`); + spinner.stop(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`); if (skippedModules.length > 0) { - console.log(chalk.yellow(`⚠️ Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`)); + await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`); } // Load existing configs and collect new fields (if any) - console.log(chalk.cyan('\n📋 Checking for new configuration options...')); + await prompts.log.info('Checking for new configuration options...'); await this.configCollector.loadExistingConfig(projectDir); let promptedForNewFields = false; @@ -2198,7 +2227,7 @@ class Installer { } if (!promptedForNewFields) { - console.log(chalk.green('✓ All configuration is up to date, no new options to configure')); + await prompts.log.success('All configuration is up to date, no new options to configure'); } // Add metadata @@ -2230,7 +2259,7 @@ class Installer { // Only succeed the spinner if it's still spinning // (install method might have stopped it if folder name changed) if (spinner.isSpinning) { - spinner.succeed('Quick update complete!'); + spinner.stop('Quick update complete!'); } return { @@ -2242,7 +2271,7 @@ class Installer { ides: configuredIdes, }; } catch (error) { - spinner.fail('Quick update failed'); + spinner.error('Quick update failed'); throw error; } } @@ -2253,12 +2282,12 @@ class Installer { * @returns {Object} Compilation result */ async compileAgents(config) { - const ora = require('ora'); - const chalk = require('chalk'); + // Using @clack prompts const { ModuleManager } = require('../modules/manager'); const { getSourcePath } = require('../../../lib/project-root'); - const spinner = ora('Recompiling agents with customizations...').start(); + const spinner = await prompts.spinner(); + spinner.start('Recompiling agents with customizations...'); try { const projectDir = path.resolve(config.directory); @@ -2266,7 +2295,7 @@ class Installer { // Check if bmad directory exists if (!(await fs.pathExists(bmadDir))) { - spinner.fail('No BMAD installation found'); + spinner.stop('No BMAD installation found'); throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`); } @@ -2308,7 +2337,7 @@ class Installer { // Process each installed module for (const moduleId of installedModules) { - spinner.text = `Recompiling agents in ${moduleId}...`; + spinner.message(`Recompiling agents in ${moduleId}...`); // Get source path let sourcePath; @@ -2324,7 +2353,7 @@ class Installer { } if (!sourcePath) { - console.log(chalk.yellow(` Warning: Source not found for module ${moduleId}, skipping...`)); + await prompts.log.warn(`Source not found for module ${moduleId}, skipping...`); continue; } @@ -2342,7 +2371,7 @@ class Installer { } } - spinner.succeed('Agent recompilation complete!'); + spinner.stop('Agent recompilation complete!'); return { success: true, @@ -2350,7 +2379,7 @@ class Installer { modules: installedModules, }; } catch (error) { - spinner.fail('Agent recompilation failed'); + spinner.error('Agent recompilation failed'); throw error; } } @@ -2372,19 +2401,14 @@ class Installer { * @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version) */ async handleLegacyV4Migration(_projectDir, _legacyV4) { - console.log(''); - console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected')); - console.log(chalk.yellow('─'.repeat(80))); - console.log(chalk.yellow('Found .bmad-method folder from BMAD v4 installation.')); - console.log(''); - - console.log(chalk.dim('Before continuing with installation, we recommend:')); - console.log(chalk.dim(' 1. Remove the .bmad-method folder, OR')); - console.log(chalk.dim(' 2. Back it up by renaming it to another name (e.g., bmad-method-backup)')); - console.log(''); - - console.log(chalk.dim('If your v4 installation set up rules or commands, you should remove those as well.')); - console.log(''); + await prompts.note( + 'Found .bmad-method folder from BMAD v4 installation.\n\n' + + 'Before continuing with installation, we recommend:\n' + + ' 1. Remove the .bmad-method folder, OR\n' + + ' 2. Back it up by renaming it to another name (e.g., bmad-method-backup)\n\n' + + 'If your v4 installation set up rules or commands, you should remove those as well.', + 'Legacy BMAD v4 detected', + ); const proceed = await prompts.select({ message: 'What would you like to do?', @@ -2404,16 +2428,11 @@ class Installer { }); if (proceed === 'exit') { - console.log(''); - console.log(chalk.cyan('Please remove the .bmad-method folder and any v4 rules/commands,')); - console.log(chalk.cyan('then run the installer again.')); - console.log(''); + await prompts.log.info('Please remove the .bmad-method folder and any v4 rules/commands, then run the installer again.'); process.exit(0); } - console.log(''); - console.log(chalk.yellow('⚠️ Proceeding with installation despite legacy v4 folder')); - console.log(''); + await prompts.log.warn('Proceeding with installation despite legacy v4 folder'); } /** @@ -2467,7 +2486,7 @@ class Installer { return files; } catch (error) { - console.warn('Warning: Could not read files-manifest.csv:', error.message); + await prompts.log.warn('Could not read files-manifest.csv: ' + error.message); return []; } } @@ -2639,22 +2658,16 @@ class Installer { }; } - // Stop any spinner for interactive prompts - const currentSpinner = ora(); - if (currentSpinner.isSpinning) { - currentSpinner.stop(); - } - - console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`)); + await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`); let keptCount = 0; let updatedCount = 0; let removedCount = 0; for (const missing of customModulesWithMissingSources) { - console.log(chalk.dim(` • ${missing.name} (${missing.id})`)); - console.log(chalk.dim(` Original source: ${missing.relativePath}`)); - console.log(chalk.dim(` Full path: ${missing.sourcePath}`)); + await prompts.log.message( + `${missing.name} (${missing.id})\n Original source: ${missing.relativePath}\n Full path: ${missing.sourcePath}`, + ); const choices = [ { @@ -2724,26 +2737,27 @@ class Installer { }); updatedCount++; - console.log(chalk.green(`✓ Updated source location`)); + await prompts.log.success('Updated source location'); break; } case 'remove': { // Extra confirmation for destructive remove - console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`)); - console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`)); + await prompts.log.error( + `WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!\n Module location: ${path.join(bmadDir, missing.id)}`, + ); const confirmDelete = await prompts.confirm({ - message: chalk.red.bold('Are you absolutely sure you want to delete this module?'), + message: 'Are you absolutely sure you want to delete this module?', default: false, }); if (confirmDelete) { const typedConfirm = await prompts.text({ - message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'), + message: 'Type "DELETE" to confirm permanent deletion:', validate: (input) => { if (input !== 'DELETE') { - return chalk.red('You must type "DELETE" exactly to proceed'); + return 'You must type "DELETE" exactly to proceed'; } return; // clack expects undefined for valid input }, @@ -2755,12 +2769,12 @@ class Installer { if (await fs.pathExists(modulePath)) { const fsExtra = require('fs-extra'); await fsExtra.remove(modulePath); - console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`)); + await prompts.log.warn(`Deleted module directory: ${path.relative(projectRoot, modulePath)}`); } await this.manifest.removeModule(bmadDir, missing.id); await this.manifest.removeCustomModule(bmadDir, missing.id); - console.log(chalk.yellow(` ✓ Removed from manifest`)); + await prompts.log.warn('Removed from manifest'); // Also remove from installedModules list if (installedModules && installedModules.includes(missing.id)) { @@ -2771,13 +2785,13 @@ class Installer { } removedCount++; - console.log(chalk.red.bold(`✓ "${missing.name}" has been permanently removed`)); + await prompts.log.error(`"${missing.name}" has been permanently removed`); } else { - console.log(chalk.dim(' Removal cancelled - module will be kept')); + await prompts.log.message('Removal cancelled - module will be kept'); keptCount++; } } else { - console.log(chalk.dim(' Removal cancelled - module will be kept')); + await prompts.log.message('Removal cancelled - module will be kept'); keptCount++; } @@ -2786,7 +2800,7 @@ class Installer { case 'keep': { keptCount++; keptModulesWithoutSources.push(missing.id); - console.log(chalk.dim(` Module will be kept as-is`)); + await prompts.log.message('Module will be kept as-is'); break; } @@ -2796,10 +2810,11 @@ class Installer { // Show summary if (keptCount > 0 || updatedCount > 0 || removedCount > 0) { - console.log(chalk.dim(`\nSummary for custom modules with missing sources:`)); - if (keptCount > 0) console.log(chalk.dim(` • ${keptCount} module(s) kept as-is`)); - if (updatedCount > 0) console.log(chalk.dim(` • ${updatedCount} module(s) updated with new sources`)); - if (removedCount > 0) console.log(chalk.red(` • ${removedCount} module(s) permanently deleted`)); + let summary = 'Summary for custom modules with missing sources:'; + if (keptCount > 0) summary += `\n • ${keptCount} module(s) kept as-is`; + if (updatedCount > 0) summary += `\n • ${updatedCount} module(s) updated with new sources`; + if (removedCount > 0) summary += `\n • ${removedCount} module(s) permanently deleted`; + await prompts.log.message(summary); } return { diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 100164d52..caea790eb 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -2,6 +2,7 @@ const path = require('node:path'); const fs = require('fs-extra'); const yaml = require('yaml'); const crypto = require('node:crypto'); +const csv = require('csv-parse/sync'); const { getSourcePath, getModulePath } = require('../../../lib/project-root'); // Load package.json for version info @@ -21,6 +22,19 @@ class ManifestGenerator { this.selectedIdes = []; } + /** + * Clean text for CSV output by normalizing whitespace and escaping quotes + * @param {string} text - Text to clean + * @returns {string} Cleaned text safe for CSV + */ + cleanForCSV(text) { + if (!text) return ''; + return text + .trim() + .replaceAll(/\s+/g, ' ') // Normalize all whitespace (including newlines) to single space + .replaceAll('"', '""'); // Escape quotes for CSV + } + /** * Generate all manifests for the installation * @param {string} bmadDir - _bmad @@ -145,7 +159,11 @@ class ManifestGenerator { // Recurse into subdirectories const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; await findWorkflows(fullPath, newRelativePath); - } else if (entry.name === 'workflow.yaml' || entry.name === 'workflow.md') { + } else if ( + entry.name === 'workflow.yaml' || + entry.name === 'workflow.md' || + (entry.name.startsWith('workflow-') && entry.name.endsWith('.md')) + ) { // Parse workflow file (both YAML and MD formats) if (debug) { console.log(`[DEBUG] Found workflow file: ${fullPath}`); @@ -161,7 +179,7 @@ class ManifestGenerator { workflow = yaml.parse(content); } else { // Parse MD workflow with YAML frontmatter - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!frontmatterMatch) { if (debug) { console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`); @@ -201,7 +219,7 @@ class ManifestGenerator { // Workflows with standalone: false are filtered out above workflows.push({ name: workflow.name, - description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV + description: this.cleanForCSV(workflow.description), module: moduleName, path: installPath, }); @@ -319,24 +337,15 @@ class ManifestGenerator { const agentName = entry.name.replace('.md', ''); - // Helper function to clean and escape CSV content - const cleanForCSV = (text) => { - if (!text) return ''; - return text - .trim() - .replaceAll(/\s+/g, ' ') // Normalize whitespace - .replaceAll('"', '""'); // Escape quotes for CSV - }; - agents.push({ name: agentName, displayName: nameMatch ? nameMatch[1] : agentName, title: titleMatch ? titleMatch[1] : '', icon: iconMatch ? iconMatch[1] : '', - role: roleMatch ? cleanForCSV(roleMatch[1]) : '', - identity: identityMatch ? cleanForCSV(identityMatch[1]) : '', - communicationStyle: styleMatch ? cleanForCSV(styleMatch[1]) : '', - principles: principlesMatch ? cleanForCSV(principlesMatch[1]) : '', + role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '', + identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '', + communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '', + principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '', module: moduleName, path: installPath, }); @@ -385,6 +394,11 @@ class ManifestGenerator { const filePath = path.join(dirPath, file); const content = await fs.readFile(filePath, 'utf8'); + // Skip internal/engine files (not user-facing tasks) + if (content.includes('internal="true"')) { + continue; + } + let name = file.replace(/\.(xml|md)$/, ''); let displayName = name; let description = ''; @@ -392,17 +406,21 @@ class ManifestGenerator { if (file.endsWith('.md')) { // Parse YAML frontmatter for .md tasks - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { try { const frontmatter = yaml.parse(frontmatterMatch[1]); name = frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name; - description = frontmatter.description || ''; - standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; + description = this.cleanForCSV(frontmatter.description || ''); + // Tasks are standalone by default unless explicitly false (internal=true is already filtered above) + standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false'; } catch { // If YAML parsing fails, use defaults + standalone = true; // Default to standalone } + } else { + standalone = true; // No frontmatter means standalone } } else { // For .xml tasks, extract from tag attributes @@ -411,10 +429,10 @@ class ManifestGenerator { const descMatch = content.match(/description="([^"]+)"/); const objMatch = content.match(/([^<]+)<\/objective>/); - description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; + description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); - const standaloneMatch = content.match(/]+standalone="true"/); - standalone = !!standaloneMatch; + const standaloneFalseMatch = content.match(/]+standalone="false"/); + standalone = !standaloneFalseMatch; } // Build relative path for installation @@ -424,7 +442,7 @@ class ManifestGenerator { tasks.push({ name: name, displayName: displayName, - description: description.replaceAll('"', '""'), + description: description, module: moduleName, path: installPath, standalone: standalone, @@ -474,6 +492,11 @@ class ManifestGenerator { const filePath = path.join(dirPath, file); const content = await fs.readFile(filePath, 'utf8'); + // Skip internal tools (same as tasks) + if (content.includes('internal="true"')) { + continue; + } + let name = file.replace(/\.(xml|md)$/, ''); let displayName = name; let description = ''; @@ -481,17 +504,21 @@ class ManifestGenerator { if (file.endsWith('.md')) { // Parse YAML frontmatter for .md tools - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { try { const frontmatter = yaml.parse(frontmatterMatch[1]); name = frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name; - description = frontmatter.description || ''; - standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; + description = this.cleanForCSV(frontmatter.description || ''); + // Tools are standalone by default unless explicitly false (internal=true is already filtered above) + standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false'; } catch { // If YAML parsing fails, use defaults + standalone = true; // Default to standalone } + } else { + standalone = true; // No frontmatter means standalone } } else { // For .xml tools, extract from tag attributes @@ -500,10 +527,10 @@ class ManifestGenerator { const descMatch = content.match(/description="([^"]+)"/); const objMatch = content.match(/([^<]+)<\/objective>/); - description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; + description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); - const standaloneMatch = content.match(/]+standalone="true"/); - standalone = !!standaloneMatch; + const standaloneFalseMatch = content.match(/]+standalone="false"/); + standalone = !standaloneFalseMatch; } // Build relative path for installation @@ -513,7 +540,7 @@ class ManifestGenerator { tools.push({ name: name, displayName: displayName, - description: description.replaceAll('"', '""'), + description: description, module: moduleName, path: installPath, standalone: standalone, @@ -706,47 +733,15 @@ class ManifestGenerator { async writeWorkflowManifest(cfgDir) { const csvPath = path.join(cfgDir, 'workflow-manifest.csv'); const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; - const parseCsvLine = (line) => { - const columns = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || []; - return columns.map((c) => c.replaceAll(/^"|"$/g, '')); - }; - - // Read existing manifest to preserve entries - const existingEntries = new Map(); - if (await fs.pathExists(csvPath)) { - const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - const parts = parseCsvLine(line); - if (parts.length >= 4) { - const [name, description, module, workflowPath] = parts; - existingEntries.set(`${module}:${name}`, { - name, - description, - module, - path: workflowPath, - }); - } - } - } - } // Create CSV header - standalone column removed, everything is canonicalized to 4 columns let csv = 'name,description,module,path\n'; - // Combine existing and new workflows + // Build workflows map from discovered workflows only + // Old entries are NOT preserved - the manifest reflects what actually exists on disk const allWorkflows = new Map(); - // Add existing entries - for (const [key, value] of existingEntries) { - allWorkflows.set(key, value); - } - - // Add/update new workflows + // Only add workflows that were actually discovered in this scan for (const workflow of this.workflows) { const key = `${workflow.module}:${workflow.name}`; allWorkflows.set(key, { @@ -773,30 +768,23 @@ class ManifestGenerator { */ async writeAgentManifest(cfgDir) { const csvPath = path.join(cfgDir, 'agent-manifest.csv'); + const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; // Read existing manifest to preserve entries const existingEntries = new Map(); if (await fs.pathExists(csvPath)) { const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - // Parse CSV (simple parsing assuming no commas in quoted fields) - const parts = line.split('","'); - if (parts.length >= 11) { - const name = parts[0].replace(/^"/, ''); - const module = parts[8]; - existingEntries.set(`${module}:${name}`, line); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with persona fields - let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n'; + let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n'; // Combine existing and new agents, preferring new data for duplicates const allAgents = new Map(); @@ -809,18 +797,38 @@ class ManifestGenerator { // Add/update new agents for (const agent of this.agents) { const key = `${agent.module}:${agent.name}`; - allAgents.set( - key, - `"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`, - ); + allAgents.set(key, { + name: agent.name, + displayName: agent.displayName, + title: agent.title, + icon: agent.icon, + role: agent.role, + identity: agent.identity, + communicationStyle: agent.communicationStyle, + principles: agent.principles, + module: agent.module, + path: agent.path, + }); } // Write all agents - for (const [, value] of allAgents) { - csv += value + '\n'; + for (const [, record] of allAgents) { + const row = [ + escapeCsv(record.name), + escapeCsv(record.displayName), + escapeCsv(record.title), + escapeCsv(record.icon), + escapeCsv(record.role), + escapeCsv(record.identity), + escapeCsv(record.communicationStyle), + escapeCsv(record.principles), + escapeCsv(record.module), + escapeCsv(record.path), + ].join(','); + csvContent += row + '\n'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } @@ -830,30 +838,23 @@ class ManifestGenerator { */ async writeTaskManifest(cfgDir) { const csvPath = path.join(cfgDir, 'task-manifest.csv'); + const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; // Read existing manifest to preserve entries const existingEntries = new Map(); if (await fs.pathExists(csvPath)) { const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - // Parse CSV (simple parsing assuming no commas in quoted fields) - const parts = line.split('","'); - if (parts.length >= 6) { - const name = parts[0].replace(/^"/, ''); - const module = parts[3]; - existingEntries.set(`${module}:${name}`, line); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with standalone column - let csv = 'name,displayName,description,module,path,standalone\n'; + let csvContent = 'name,displayName,description,module,path,standalone\n'; // Combine existing and new tasks const allTasks = new Map(); @@ -866,15 +867,30 @@ class ManifestGenerator { // Add/update new tasks for (const task of this.tasks) { const key = `${task.module}:${task.name}`; - allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`); + allTasks.set(key, { + name: task.name, + displayName: task.displayName, + description: task.description, + module: task.module, + path: task.path, + standalone: task.standalone, + }); } // Write all tasks - for (const [, value] of allTasks) { - csv += value + '\n'; + for (const [, record] of allTasks) { + const row = [ + escapeCsv(record.name), + escapeCsv(record.displayName), + escapeCsv(record.description), + escapeCsv(record.module), + escapeCsv(record.path), + escapeCsv(record.standalone), + ].join(','); + csvContent += row + '\n'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } @@ -884,30 +900,23 @@ class ManifestGenerator { */ async writeToolManifest(cfgDir) { const csvPath = path.join(cfgDir, 'tool-manifest.csv'); + const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; // Read existing manifest to preserve entries const existingEntries = new Map(); if (await fs.pathExists(csvPath)) { const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - // Parse CSV (simple parsing assuming no commas in quoted fields) - const parts = line.split('","'); - if (parts.length >= 6) { - const name = parts[0].replace(/^"/, ''); - const module = parts[3]; - existingEntries.set(`${module}:${name}`, line); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with standalone column - let csv = 'name,displayName,description,module,path,standalone\n'; + let csvContent = 'name,displayName,description,module,path,standalone\n'; // Combine existing and new tools const allTools = new Map(); @@ -920,15 +929,30 @@ class ManifestGenerator { // Add/update new tools for (const tool of this.tools) { const key = `${tool.module}:${tool.name}`; - allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`); + allTools.set(key, { + name: tool.name, + displayName: tool.displayName, + description: tool.description, + module: tool.module, + path: tool.path, + standalone: tool.standalone, + }); } // Write all tools - for (const [, value] of allTools) { - csv += value + '\n'; + for (const [, record] of allTools) { + const row = [ + escapeCsv(record.name), + escapeCsv(record.displayName), + escapeCsv(record.description), + escapeCsv(record.module), + escapeCsv(record.path), + escapeCsv(record.standalone), + ].join(','); + csvContent += row + '\n'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } diff --git a/tools/cli/installers/lib/custom/handler.js b/tools/cli/installers/lib/custom/handler.js index c8aa52eee..6256e3cd2 100644 --- a/tools/cli/installers/lib/custom/handler.js +++ b/tools/cli/installers/lib/custom/handler.js @@ -1,7 +1,7 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); const yaml = require('yaml'); +const prompts = require('../../../lib/prompts'); const { FileOps } = require('../../../lib/file-ops'); const { XmlHandler } = require('../../../lib/xml-handler'); @@ -88,7 +88,7 @@ class CustomHandler { try { config = yaml.parse(configContent); } catch (parseError) { - console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message)); + await prompts.log.warn('YAML parse error in ' + configPath + ': ' + parseError.message); return null; } @@ -111,7 +111,7 @@ class CustomHandler { isInstallConfig: isInstallConfig, // Track which type this is }; } catch (error) { - console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message)); + await prompts.log.warn('Failed to read ' + configPath + ': ' + error.message); return null; } } @@ -268,14 +268,13 @@ class CustomHandler { } results.filesCopied++; + if (entry.name.endsWith('.md')) { + results.workflowsInstalled++; + } if (fileTrackingCallback) { fileTrackingCallback(targetPath); } } - - if (entry.name.endsWith('.md')) { - results.workflowsInstalled++; - } } catch (error) { results.errors.push(`Failed to copy ${entry.name}: ${error.message}`); } @@ -297,7 +296,7 @@ class CustomHandler { const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']); for (const agentFile of agentFiles) { - const relativePath = path.relative(sourceAgentsPath, agentFile); + const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/'); const targetDir = path.join(targetAgentsPath, path.dirname(relativePath)); await fs.ensureDir(targetDir); @@ -322,7 +321,7 @@ class CustomHandler { await fs.writeFile(customizePath, templateContent, 'utf8'); // Only show customize creation in verbose mode if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`)); + await prompts.log.message(' Created customize: custom-' + agentName + '.customize.yaml'); } } } @@ -346,14 +345,10 @@ class CustomHandler { // Only show compilation details in verbose mode if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log( - chalk.dim( - ` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`, - ), - ); + await prompts.log.message(' Compiled agent: ' + agentName + ' -> ' + path.relative(targetAgentsPath, targetMdPath)); } } catch (error) { - console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message)); + await prompts.log.warn(' Failed to compile agent ' + agentName + ': ' + error.message); results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`); } } diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js index 60c38dd5d..442eb0841 100644 --- a/tools/cli/installers/lib/ide/_base-ide.js +++ b/tools/cli/installers/lib/ide/_base-ide.js @@ -1,8 +1,9 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); const { XmlHandler } = require('../../../lib/xml-handler'); +const prompts = require('../../../lib/prompts'); const { getSourcePath } = require('../../../lib/project-root'); +const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); /** * Base class for IDE-specific setup @@ -18,7 +19,7 @@ class BaseIdeSetup { this.configFile = null; // Override in subclasses when detection is file-based this.detectionPaths = []; // Additional paths that indicate the IDE is configured this.xmlHandler = new XmlHandler(); - this.bmadFolderName = 'bmad'; // Default, can be overridden + this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden } /** @@ -52,15 +53,15 @@ class BaseIdeSetup { * Cleanup IDE configuration * @param {string} projectDir - Project directory */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { // Default implementation - can be overridden if (this.configDir) { const configPath = path.join(projectDir, this.configDir); if (await fs.pathExists(configPath)) { - const bmadRulesPath = path.join(configPath, 'bmad'); + const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME); if (await fs.pathExists(bmadRulesPath)) { await fs.remove(bmadRulesPath); - console.log(chalk.dim(`Removed ${this.name} BMAD configuration`)); + if (!options.silent) await prompts.log.message(`Removed ${this.name} BMAD configuration`); } } } @@ -351,13 +352,15 @@ class BaseIdeSetup { const workflowData = yaml.parse(content); if (workflowData && workflowData.name) { + // Workflows are standalone by default unless explicitly false + const standalone = workflowData.standalone !== false && workflowData.standalone !== 'false'; workflows.push({ name: workflowData.name, path: fullPath, relativePath: path.relative(dir, fullPath), filename: entry.name, description: workflowData.description || '', - standalone: workflowData.standalone === true, // Check standalone property + standalone: standalone, }); } } catch { @@ -441,31 +444,38 @@ class BaseIdeSetup { const matchedExt = extensions.find((e) => entry.name.endsWith(e)); if (matchedExt) { // Read file content to check for standalone attribute - let standalone = false; + // All non-internal files are considered standalone by default + let standalone = true; try { const content = await fs.readFile(fullPath, 'utf8'); - // Check for standalone="true" in XML files + // Skip internal/engine files (not user-facing) + if (content.includes('internal="true"')) { + continue; + } + + // Check for explicit standalone: false if (entry.name.endsWith('.xml')) { - // Look for standalone="true" in the opening tag (task or tool) - const standaloneMatch = content.match(/<(?:task|tool)[^>]+standalone="true"/); - standalone = !!standaloneMatch; + // For XML files, check for standalone="false" attribute + const tagMatch = content.match(/<(task|tool)[^>]*standalone="false"/); + standalone = !tagMatch; } else if (entry.name.endsWith('.md')) { - // Check for standalone: true in YAML frontmatter - const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); + // For MD files, parse YAML frontmatter + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { - const yaml = require('yaml'); try { + const yaml = require('yaml'); const frontmatter = yaml.parse(frontmatterMatch[1]); - standalone = frontmatter.standalone === true; + standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false'; } catch { - // Ignore YAML parse errors + // If YAML parsing fails, default to standalone } } + // No frontmatter means standalone (default) } } catch { - // If we can't read the file, assume not standalone - standalone = false; + // If we can't read the file, default to standalone + standalone = true; } files.push({ diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 87be7300a..7eb2533ed 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -1,7 +1,7 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); const { BaseIdeSetup } = require('./_base-ide'); +const prompts = require('../../../lib/prompts'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); @@ -34,10 +34,10 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { * @returns {Promise} Setup result */ async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); // Clean up any old BMAD installation first - await this.cleanup(projectDir); + await this.cleanup(projectDir, options); if (!this.installerConfig) { return { success: false, reason: 'no-config' }; @@ -66,6 +66,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { */ async installToTarget(projectDir, bmadDir, config, options) { const { target_dir, template_type, artifact_types } = config; + + // Skip targets with explicitly empty artifact_types array + // This prevents creating empty directories when no artifacts will be written + if (Array.isArray(artifact_types) && artifact_types.length === 0) { + return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } }; + } + const targetPath = path.join(projectDir, target_dir); await this.ensureDir(targetPath); @@ -86,15 +93,16 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); } - // Install tasks and tools + // Install tasks and tools using template system (supports TOML for Gemini, MD for others) if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) { - const taskToolGen = new TaskToolCommandGenerator(); - const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath); + const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); + const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); + const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config); results.tasks = taskToolResult.tasks || 0; results.tools = taskToolResult.tools || 0; } - this.printSummary(results, target_dir); + await this.printSummary(results, target_dir, options); return { success: true, results }; } @@ -180,6 +188,53 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { return count; } + /** + * Write task/tool artifacts to target directory using templates + * @param {string} targetPath - Target directory path + * @param {Array} artifacts - Task/tool artifacts + * @param {string} templateType - Template type to use + * @param {Object} config - Installation configuration + * @returns {Promise} Counts of tasks and tools written + */ + async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}) { + let taskCount = 0; + let toolCount = 0; + + // Pre-load templates to avoid repeated file I/O in the loop + const taskTemplate = await this.loadTemplate(templateType, 'task', config, 'default-task'); + const toolTemplate = await this.loadTemplate(templateType, 'tool', config, 'default-tool'); + + const { artifact_types } = config; + + for (const artifact of artifacts) { + if (artifact.type !== 'task' && artifact.type !== 'tool') { + continue; + } + + // Skip if the specific artifact type is not requested in config + if (artifact_types) { + if (artifact.type === 'task' && !artifact_types.includes('tasks')) continue; + if (artifact.type === 'tool' && !artifact_types.includes('tools')) continue; + } + + // Use pre-loaded template based on artifact type + const { content: template, extension } = artifact.type === 'task' ? taskTemplate : toolTemplate; + + const content = this.renderTemplate(template, artifact); + const filename = this.generateFilename(artifact, artifact.type, extension); + const filePath = path.join(targetPath, filename); + await this.writeFile(filePath, content); + + if (artifact.type === 'task') { + taskCount++; + } else { + toolCount++; + } + } + + return { tasks: taskCount, tools: toolCount }; + } + /** * Load template based on type and configuration * @param {string} templateType - Template type (claude, windsurf, etc.) @@ -316,10 +371,24 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} renderTemplate(template, artifact) { // Use the appropriate path property based on artifact type let pathToUse = artifact.relativePath || ''; - if (artifact.type === 'agent-launcher') { - pathToUse = artifact.agentPath || artifact.relativePath || ''; - } else if (artifact.type === 'workflow-command') { - pathToUse = artifact.workflowPath || artifact.relativePath || ''; + switch (artifact.type) { + case 'agent-launcher': { + pathToUse = artifact.agentPath || artifact.relativePath || ''; + + break; + } + case 'workflow-command': { + pathToUse = artifact.workflowPath || artifact.relativePath || ''; + + break; + } + case 'task': + case 'tool': { + pathToUse = artifact.path || artifact.relativePath || ''; + + break; + } + // No default } let rendered = template @@ -351,8 +420,9 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} // Reuse central logic to ensure consistent naming conventions const standardName = toDashPath(artifact.relativePath); - // Clean up potential double extensions from source files (e.g. .yaml.md -> .md) - const baseName = standardName.replace(/\.(yaml|yml)\.md$/, '.md'); + // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md) + // This handles any extensions that might slip through toDashPath() + const baseName = standardName.replace(/\.(md|yaml|yml|json|xml|toml)\.md$/i, '.md'); // If using default markdown, preserve the bmad-agent- prefix for agents if (extension === '.md') { @@ -369,32 +439,28 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * @param {Object} results - Installation results * @param {string} targetDir - Target directory (relative) */ - printSummary(results, targetDir) { - console.log(chalk.green(`\n✓ ${this.name} configured:`)); - if (results.agents > 0) { - console.log(chalk.dim(` - ${results.agents} agents installed`)); - } - if (results.workflows > 0) { - console.log(chalk.dim(` - ${results.workflows} workflow commands generated`)); - } - if (results.tasks > 0 || results.tools > 0) { - console.log(chalk.dim(` - ${results.tasks + results.tools} task/tool commands generated`)); - } - console.log(chalk.dim(` - Destination: ${targetDir}`)); + async printSummary(results, targetDir, options = {}) { + if (options.silent) return; + const parts = []; + if (results.agents > 0) parts.push(`${results.agents} agents`); + if (results.workflows > 0) parts.push(`${results.workflows} workflows`); + if (results.tasks > 0) parts.push(`${results.tasks} tasks`); + if (results.tools > 0) parts.push(`${results.tools} tools`); + await prompts.log.success(`${this.name} configured: ${parts.join(', ')} → ${targetDir}`); } /** * Cleanup IDE configuration * @param {string} projectDir - Project directory */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { // Clean all target directories if (this.installerConfig?.targets) { for (const target of this.installerConfig.targets) { - await this.cleanupTarget(projectDir, target.target_dir); + await this.cleanupTarget(projectDir, target.target_dir, options); } } else if (this.installerConfig?.target_dir) { - await this.cleanupTarget(projectDir, this.installerConfig.target_dir); + await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options); } } @@ -403,7 +469,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * @param {string} projectDir - Project directory * @param {string} targetDir - Target directory to clean */ - async cleanupTarget(projectDir, targetDir) { + async cleanupTarget(projectDir, targetDir, options = {}) { const targetPath = path.join(projectDir, targetDir); if (!(await fs.pathExists(targetPath))) { @@ -426,25 +492,22 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} let removedCount = 0; for (const entry of entries) { - // Skip non-strings or undefined entries if (!entry || typeof entry !== 'string') { continue; } if (entry.startsWith('bmad')) { const entryPath = path.join(targetPath, entry); - const stat = await fs.stat(entryPath); - if (stat.isFile()) { - await fs.remove(entryPath); - removedCount++; - } else if (stat.isDirectory()) { + try { await fs.remove(entryPath); removedCount++; + } catch { + // Skip entries that can't be removed (broken symlinks, permission errors) } } } - if (removedCount > 0) { - console.log(chalk.dim(` Cleaned ${removedCount} BMAD files from ${targetDir}`)); + if (removedCount > 0 && !options.silent) { + await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`); } } } diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index 5cd503e24..8e91e003b 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -1,7 +1,6 @@ const path = require('node:path'); const fs = require('fs-extra'); const os = require('node:os'); -const chalk = require('chalk'); const { BaseIdeSetup } = require('./_base-ide'); const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); @@ -43,12 +42,11 @@ class CodexSetup extends BaseIdeSetup { default: 'global', }); - // Display detailed instructions for the chosen option - console.log(''); + // Show brief confirmation hint (detailed instructions available via verbose) if (installLocation === 'project') { - console.log(this.getProjectSpecificInstructions()); + await prompts.log.info('Prompts installed to: /.codex/prompts (requires CODEX_HOME)'); } else { - console.log(this.getGlobalInstructions()); + await prompts.log.info('Prompts installed to: ~/.codex/prompts'); } // Confirm the choice @@ -58,7 +56,7 @@ class CodexSetup extends BaseIdeSetup { }); if (!confirmed) { - console.log(chalk.yellow("\n Let's choose a different installation option.\n")); + await prompts.log.warn("Let's choose a different installation option."); } } @@ -72,7 +70,7 @@ class CodexSetup extends BaseIdeSetup { * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); // Always use CLI mode const mode = 'cli'; @@ -84,7 +82,7 @@ class CodexSetup extends BaseIdeSetup { const destDir = this.getCodexPromptDir(projectDir, installLocation); await fs.ensureDir(destDir); - await this.clearOldBmadFiles(destDir); + await this.clearOldBmadFiles(destDir, options); // Collect artifacts and write using underscore format const agentGen = new AgentCommandGenerator(this.bmadFolderName); @@ -104,7 +102,10 @@ class CodexSetup extends BaseIdeSetup { ); taskArtifacts.push({ type: 'task', + name: task.name, + displayName: task.name, module: task.module, + path: task.path, sourcePath: task.path, relativePath: path.join(task.module, 'tasks', `${task.name}.md`), content, @@ -116,21 +117,16 @@ class CodexSetup extends BaseIdeSetup { const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts); // Also write tasks using underscore format - const ttGen = new TaskToolCommandGenerator(); + const ttGen = new TaskToolCommandGenerator(this.bmadFolderName); const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts); const written = agentCount + workflowCount + tasksWritten; - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - Mode: CLI`)); - console.log(chalk.dim(` - ${counts.agents} agents exported`)); - console.log(chalk.dim(` - ${counts.tasks} tasks exported`)); - console.log(chalk.dim(` - ${counts.workflows} workflow commands exported`)); - if (counts.workflowLaunchers > 0) { - console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers exported`)); + if (!options.silent) { + await prompts.log.success( + `${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} files → ${destDir}`, + ); } - console.log(chalk.dim(` - ${written} Codex prompt files written`)); - console.log(chalk.dim(` - Destination: ${destDir}`)); return { success: true, @@ -214,7 +210,10 @@ class CodexSetup extends BaseIdeSetup { artifacts.push({ type: 'task', + name: task.name, + displayName: task.name, module: task.module, + path: task.path, sourcePath: task.path, relativePath: path.join(task.module, 'tasks', `${task.name}.md`), content, @@ -256,7 +255,7 @@ class CodexSetup extends BaseIdeSetup { return written; } - async clearOldBmadFiles(destDir) { + async clearOldBmadFiles(destDir, options = {}) { if (!(await fs.pathExists(destDir))) { return; } @@ -266,7 +265,7 @@ class CodexSetup extends BaseIdeSetup { entries = await fs.readdir(destDir); } catch (error) { // Directory exists but can't be read - skip cleanup - console.warn(chalk.yellow(`Warning: Could not read directory ${destDir}: ${error.message}`)); + if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`); return; } @@ -285,15 +284,11 @@ class CodexSetup extends BaseIdeSetup { const entryPath = path.join(destDir, entry); try { - const stat = await fs.stat(entryPath); - if (stat.isFile()) { - await fs.remove(entryPath); - } else if (stat.isDirectory()) { - await fs.remove(entryPath); - } + await fs.remove(entryPath); } catch (error) { - // Skip files that can't be processed - console.warn(chalk.dim(` Skipping ${entry}: ${error.message}`)); + if (!options.silent) { + await prompts.log.message(` Skipping ${entry}: ${error.message}`); + } } } } @@ -309,22 +304,16 @@ class CodexSetup extends BaseIdeSetup { */ getGlobalInstructions(destDir) { const lines = [ + 'IMPORTANT: Codex Configuration', '', - chalk.bold.cyan('═'.repeat(70)), - chalk.bold.yellow(' IMPORTANT: Codex Configuration'), - chalk.bold.cyan('═'.repeat(70)), + '/prompts installed globally to your HOME DIRECTORY.', '', - chalk.white(' /prompts installed globally to your HOME DIRECTORY.'), - '', - chalk.yellow(' ⚠️ These prompts reference a specific _bmad path'), - chalk.dim(" To use with other projects, you'd need to copy the _bmad dir"), - '', - chalk.green(' ✓ You can now use /commands in Codex CLI'), - chalk.dim(' Example: /bmad_bmm_pm'), - chalk.dim(' Type / to see all available commands'), - '', - chalk.bold.cyan('═'.repeat(70)), + 'These prompts reference a specific _bmad path.', + "To use with other projects, you'd need to copy the _bmad dir.", '', + 'You can now use /commands in Codex CLI', + ' Example: /bmad_bmm_pm', + ' Type / to see all available commands', ]; return lines.join('\n'); } @@ -339,43 +328,34 @@ class CodexSetup extends BaseIdeSetup { const isWindows = os.platform() === 'win32'; const commonLines = [ + 'Project-Specific Codex Configuration', '', - chalk.bold.cyan('═'.repeat(70)), - chalk.bold.yellow(' Project-Specific Codex Configuration'), - chalk.bold.cyan('═'.repeat(70)), + `Prompts will be installed to: ${destDir || '/.codex/prompts'}`, '', - chalk.white(' Prompts will be installed to: ') + chalk.cyan(destDir || '/.codex/prompts'), - '', - chalk.bold.yellow(' ⚠️ REQUIRED: You must set CODEX_HOME to use these prompts'), + 'REQUIRED: You must set CODEX_HOME to use these prompts', '', ]; const windowsLines = [ - chalk.bold(' Create a codex.cmd file in your project root:'), + 'Create a codex.cmd file in your project root:', '', - chalk.green(' @echo off'), - chalk.green(' set CODEX_HOME=%~dp0.codex'), - chalk.green(' codex %*'), + ' @echo off', + ' set CODEX_HOME=%~dp0.codex', + ' codex %*', '', - chalk.dim(String.raw` Then run: .\codex instead of codex`), - chalk.dim(' (The %~dp0 gets the directory of the .cmd file)'), + String.raw`Then run: .\codex instead of codex`, + '(The %~dp0 gets the directory of the .cmd file)', ]; const unixLines = [ - chalk.bold(' Add this alias to your ~/.bashrc or ~/.zshrc:'), + 'Add this alias to your ~/.bashrc or ~/.zshrc:', '', - chalk.green(' alias codex=\'CODEX_HOME="$PWD/.codex" codex\''), - '', - chalk.dim(' After adding, run: source ~/.bashrc (or source ~/.zshrc)'), - chalk.dim(' (The $PWD uses your current working directory)'), - ]; - const closingLines = [ - '', - chalk.dim(' This tells Codex CLI to use prompts from this project instead of ~/.codex'), - '', - chalk.bold.cyan('═'.repeat(70)), + ' alias codex=\'CODEX_HOME="$PWD/.codex" codex\'', '', + 'After adding, run: source ~/.bashrc (or source ~/.zshrc)', + '(The $PWD uses your current working directory)', ]; + const closingLines = ['', 'This tells Codex CLI to use prompts from this project instead of ~/.codex']; const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines]; diff --git a/tools/cli/installers/lib/ide/kilo.js b/tools/cli/installers/lib/ide/kilo.js index 45e380218..2e5734391 100644 --- a/tools/cli/installers/lib/ide/kilo.js +++ b/tools/cli/installers/lib/ide/kilo.js @@ -1,7 +1,10 @@ const path = require('node:path'); const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); +const yaml = require('yaml'); +const prompts = require('../../../lib/prompts'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); +const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); /** * KiloCode IDE setup handler @@ -20,78 +23,93 @@ class KiloSetup extends BaseIdeSetup { * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); - // Check for existing .kilocodemodes file + // Clean up any old BMAD installation first + await this.cleanup(projectDir, options); + + // Load existing config (may contain non-BMAD modes and other settings) const kiloModesPath = path.join(projectDir, this.configFile); - let existingModes = []; - let existingContent = ''; + let config = {}; if (await this.pathExists(kiloModesPath)) { - existingContent = await this.readFile(kiloModesPath); - // Parse existing modes - const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g); - for (const match of modeMatches) { - existingModes.push(match[1]); + const existingContent = await this.readFile(kiloModesPath); + try { + config = yaml.parse(existingContent) || {}; + } catch { + // If parsing fails, start fresh but warn user + await prompts.log.warn('Warning: Could not parse existing .kilocodemodes, starting fresh'); + config = {}; } - console.log(chalk.yellow(`Found existing .kilocodemodes file with ${existingModes.length} modes`)); + } + + // Ensure customModes array exists + if (!Array.isArray(config.customModes)) { + config.customModes = []; } // Generate agent launchers const agentGen = new AgentCommandGenerator(this.bmadFolderName); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - // Create modes content - let newModesContent = ''; + // Create mode objects and add to config let addedCount = 0; - let skippedCount = 0; for (const artifact of agentArtifacts) { - const slug = `bmad-${artifact.module}-${artifact.name}`; - - // Skip if already exists - if (existingModes.includes(slug)) { - console.log(chalk.dim(` Skipping ${slug} - already exists`)); - skippedCount++; - continue; - } - - const modeEntry = await this.createModeEntry(artifact, projectDir); - - newModesContent += modeEntry; + const modeObject = await this.createModeObject(artifact, projectDir); + config.customModes.push(modeObject); addedCount++; } - // Build final content - let finalContent = ''; - if (existingContent) { - finalContent = existingContent.trim() + '\n' + newModesContent; - } else { - finalContent = 'customModes:\n' + newModesContent; - } - - // Write .kilocodemodes file + // Write .kilocodemodes file with proper YAML structure + const finalContent = yaml.stringify(config, { lineWidth: 0 }); await this.writeFile(kiloModesPath, finalContent); - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${addedCount} modes added`)); - if (skippedCount > 0) { - console.log(chalk.dim(` - ${skippedCount} modes skipped (already exist)`)); + // Generate workflow commands + const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); + + // Write to .kilocode/workflows/ directory + const workflowsDir = path.join(projectDir, '.kilocode', 'workflows'); + await this.ensureDir(workflowsDir); + + // Clear old BMAD workflows before writing new ones + await this.clearBmadWorkflows(workflowsDir); + + // Write workflow files + const workflowCount = await workflowGenerator.writeDashArtifacts(workflowsDir, workflowArtifacts); + + // Generate task and tool commands + const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); + const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); + + // Write task/tool files to workflows directory (same location as workflows) + await taskToolGen.writeDashArtifacts(workflowsDir, taskToolArtifacts); + const taskCount = taskToolCounts.tasks || 0; + const toolCount = taskToolCounts.tools || 0; + + if (!options.silent) { + await prompts.log.success( + `${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`, + ); } - console.log(chalk.dim(` - Configuration file: ${this.configFile}`)); - console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode')); return { success: true, modes: addedCount, - skipped: skippedCount, + workflows: workflowCount, + tasks: taskCount, + tools: toolCount, }; } /** - * Create a mode entry for an agent + * Create a mode object for an agent + * @param {Object} artifact - Agent artifact + * @param {string} projectDir - Project directory + * @returns {Object} Mode object for YAML serialization */ - async createModeEntry(artifact, projectDir) { + async createModeObject(artifact, projectDir) { // Extract metadata from launcher content const titleMatch = artifact.content.match(/title="([^"]+)"/); const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name); @@ -102,8 +120,8 @@ class KiloSetup extends BaseIdeSetup { const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/); const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; - // Get the activation header from central template - const activationHeader = await this.getAgentCommandHeader(); + // Get the activation header from central template (trim to avoid YAML formatting issues) + const activationHeader = (await this.getAgentCommandHeader()).trim(); const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/); const roleDefinition = roleDefinitionMatch @@ -113,22 +131,15 @@ class KiloSetup extends BaseIdeSetup { // Get relative path const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/'); - // Build mode entry (KiloCode uses same schema as Roo) - const slug = `bmad-${artifact.module}-${artifact.name}`; - let modeEntry = ` - slug: ${slug}\n`; - modeEntry += ` name: '${icon} ${title}'\n`; - modeEntry += ` roleDefinition: ${roleDefinition}\n`; - modeEntry += ` whenToUse: ${whenToUse}\n`; - modeEntry += ` customInstructions: |\n`; - modeEntry += ` ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`; - modeEntry += ` groups:\n`; - modeEntry += ` - read\n`; - modeEntry += ` - edit\n`; - modeEntry += ` - browser\n`; - modeEntry += ` - command\n`; - modeEntry += ` - mcp\n`; - - return modeEntry; + // Build mode object (KiloCode uses same schema as Roo) + return { + slug: `bmad-${artifact.module}-${artifact.name}`, + name: `${icon} ${title}`, + roleDefinition: roleDefinition, + whenToUse: whenToUse, + customInstructions: `${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`, + groups: ['read', 'edit', 'browser', 'command', 'mcp'], + }; } /** @@ -141,38 +152,55 @@ class KiloSetup extends BaseIdeSetup { .join(' '); } + /** + * Clear old BMAD workflow files from workflows directory + * @param {string} workflowsDir - Workflows directory path + */ + async clearBmadWorkflows(workflowsDir) { + const fs = require('fs-extra'); + if (!(await fs.pathExists(workflowsDir))) return; + + const entries = await fs.readdir(workflowsDir); + for (const entry of entries) { + if (entry.startsWith('bmad-') && entry.endsWith('.md')) { + await fs.remove(path.join(workflowsDir, entry)); + } + } + } + /** * Cleanup KiloCode configuration */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { const fs = require('fs-extra'); const kiloModesPath = path.join(projectDir, this.configFile); if (await fs.pathExists(kiloModesPath)) { const content = await fs.readFile(kiloModesPath, 'utf8'); - // Remove BMAD modes only - const lines = content.split('\n'); - const filteredLines = []; - let skipMode = false; - let removedCount = 0; + try { + const config = yaml.parse(content) || {}; - for (const line of lines) { - if (/^\s*- slug: bmad-/.test(line)) { - skipMode = true; - removedCount++; - } else if (skipMode && /^\s*- slug: /.test(line)) { - skipMode = false; - } + if (Array.isArray(config.customModes)) { + const originalCount = config.customModes.length; + // Remove BMAD modes only (keep non-BMAD modes) + config.customModes = config.customModes.filter((mode) => !mode.slug || !mode.slug.startsWith('bmad-')); + const removedCount = originalCount - config.customModes.length; - if (!skipMode) { - filteredLines.push(line); + if (removedCount > 0) { + await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 })); + if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .kilocodemodes`); + } } + } catch { + // If parsing fails, leave file as-is + if (!options.silent) await prompts.log.warn('Warning: Could not parse .kilocodemodes for cleanup'); } - - await fs.writeFile(kiloModesPath, filteredLines.join('\n')); - console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`)); } + + // Clean up workflow files + const workflowsDir = path.join(projectDir, '.kilocode', 'workflows'); + await this.clearBmadWorkflows(workflowsDir); } /** @@ -185,31 +213,28 @@ class KiloSetup extends BaseIdeSetup { */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { const kilocodemodesPath = path.join(projectDir, this.configFile); - let existingContent = ''; + let config = {}; // Read existing .kilocodemodes file if (await this.pathExists(kilocodemodesPath)) { - existingContent = await this.readFile(kilocodemodesPath); + const existingContent = await this.readFile(kilocodemodesPath); + try { + config = yaml.parse(existingContent) || {}; + } catch { + config = {}; + } } - // Create custom agent mode entry + // Ensure customModes array exists + if (!Array.isArray(config.customModes)) { + config.customModes = []; + } + + // Create custom agent mode object const slug = `bmad-custom-${agentName.toLowerCase()}`; - const modeEntry = ` - slug: ${slug} - name: 'BMAD Custom: ${agentName}' - description: | - Custom BMAD agent: ${agentName} - - **⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - - This is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file. - prompt: | - @${agentPath} - always: false - permissions: all -`; // Check if mode already exists - if (existingContent.includes(slug)) { + if (config.customModes.some((mode) => mode.slug === slug)) { return { ide: 'kilo', path: this.configFile, @@ -219,24 +244,18 @@ class KiloSetup extends BaseIdeSetup { }; } - // Build final content - let finalContent = ''; - if (existingContent) { - // Find customModes section or add it - if (existingContent.includes('customModes:')) { - // Append to existing customModes - finalContent = existingContent + modeEntry; - } else { - // Add customModes section - finalContent = existingContent.trim() + '\n\ncustomModes:\n' + modeEntry; - } - } else { - // Create new .kilocodemodes file with customModes - finalContent = 'customModes:\n' + modeEntry; - } + // Add custom mode object + config.customModes.push({ + slug: slug, + name: `BMAD Custom: ${agentName}`, + description: `Custom BMAD agent: ${agentName}\n\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\nThis is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.\n`, + prompt: `@${agentPath}\n`, + always: false, + permissions: 'all', + }); - // Write .kilocodemodes file - await this.writeFile(kilocodemodesPath, finalContent); + // Write .kilocodemodes file with proper YAML structure + await this.writeFile(kilocodemodesPath, yaml.stringify(config, { lineWidth: 0 })); return { ide: 'kilo', diff --git a/tools/cli/installers/lib/ide/kiro-cli.js b/tools/cli/installers/lib/ide/kiro-cli.js index 612ea5fa4..150dca189 100644 --- a/tools/cli/installers/lib/ide/kiro-cli.js +++ b/tools/cli/installers/lib/ide/kiro-cli.js @@ -1,7 +1,7 @@ const path = require('node:path'); const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); const fs = require('fs-extra'); +const prompts = require('../../../lib/prompts'); const yaml = require('yaml'); /** @@ -18,7 +18,7 @@ class KiroCliSetup extends BaseIdeSetup { * Cleanup old BMAD installation before reinstalling * @param {string} projectDir - Project directory */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { const bmadAgentsDir = path.join(projectDir, this.configDir, this.agentsDir); if (await fs.pathExists(bmadAgentsDir)) { @@ -29,7 +29,7 @@ class KiroCliSetup extends BaseIdeSetup { await fs.remove(path.join(bmadAgentsDir, file)); } } - console.log(chalk.dim(` Cleaned old BMAD agents from ${this.name}`)); + if (!options.silent) await prompts.log.message(` Cleaned old BMAD agents from ${this.name}`); } } @@ -40,9 +40,9 @@ class KiroCliSetup extends BaseIdeSetup { * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); - await this.cleanup(projectDir); + await this.cleanup(projectDir, options); const kiroDir = path.join(projectDir, this.configDir); const agentsDir = path.join(kiroDir, this.agentsDir); @@ -52,7 +52,7 @@ class KiroCliSetup extends BaseIdeSetup { // Create BMad agents from source YAML files await this.createBmadAgentsFromSource(agentsDir, projectDir); - console.log(chalk.green(`✓ ${this.name} configured with BMad agents`)); + if (!options.silent) await prompts.log.success(`${this.name} configured with BMad agents`); } /** @@ -70,7 +70,7 @@ class KiroCliSetup extends BaseIdeSetup { try { await this.processAgentFile(agentFile, agentsDir, projectDir); } catch (error) { - console.warn(chalk.yellow(`⚠️ Failed to process ${agentFile}: ${error.message}`)); + await prompts.log.warn(`Failed to process ${agentFile}: ${error.message}`); } } } diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 9febbe7c5..7e5da1ad5 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -1,6 +1,7 @@ const fs = require('fs-extra'); const path = require('node:path'); -const chalk = require('chalk'); +const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); +const prompts = require('../../../lib/prompts'); /** * IDE Manager - handles IDE-specific setup @@ -14,7 +15,7 @@ class IdeManager { constructor() { this.handlers = new Map(); this._initialized = false; - this.bmadFolderName = 'bmad'; // Default, can be overridden + this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden } /** @@ -48,7 +49,7 @@ class IdeManager { */ async loadHandlers() { // Load custom installer files - this.loadCustomInstallerFiles(); + await this.loadCustomInstallerFiles(); // Load config-driven handlers from platform-codes.yaml await this.loadConfigDrivenHandlers(); @@ -58,7 +59,7 @@ class IdeManager { * Load custom installer files (unique installation logic) * These files have special installation patterns that don't fit the config-driven model */ - loadCustomInstallerFiles() { + async loadCustomInstallerFiles() { const ideDir = __dirname; const customFiles = ['codex.js', 'kilo.js', 'kiro-cli.js', 'mistral-vibe.js']; @@ -73,11 +74,14 @@ class IdeManager { if (HandlerClass) { const instance = new HandlerClass(); if (instance.name && typeof instance.name === 'string') { + if (typeof instance.setBmadFolderName === 'function') { + instance.setBmadFolderName(this.bmadFolderName); + } this.handlers.set(instance.name, instance); } } } catch (error) { - console.log(chalk.yellow(` Warning: Could not load ${file}: ${error.message}`)); + await prompts.log.warn(`Warning: Could not load ${file}: ${error.message}`); } } } @@ -100,7 +104,9 @@ class IdeManager { if (!platformInfo.installer) continue; const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo); - handler.setBmadFolderName(this.bmadFolderName); + if (typeof handler.setBmadFolderName === 'function') { + handler.setBmadFolderName(this.bmadFolderName); + } this.handlers.set(platformCode, handler); } } @@ -165,17 +171,45 @@ class IdeManager { const handler = this.handlers.get(ideName.toLowerCase()); if (!handler) { - console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported`)); - console.log(chalk.dim('Supported IDEs:', [...this.handlers.keys()].join(', '))); - return { success: false, reason: 'unsupported' }; + await prompts.log.warn(`IDE '${ideName}' is not yet supported`); + await prompts.log.message(`Supported IDEs: ${[...this.handlers.keys()].join(', ')}`); + return { success: false, ide: ideName, error: 'unsupported IDE' }; } try { - await handler.setup(projectDir, bmadDir, options); - return { success: true, ide: ideName }; + const handlerResult = await handler.setup(projectDir, bmadDir, options); + // Build detail string from handler-returned data + let detail = ''; + if (handlerResult && handlerResult.results) { + // Config-driven handlers return { success, results: { agents, workflows, tasks, tools } } + const r = handlerResult.results; + const parts = []; + if (r.agents > 0) parts.push(`${r.agents} agents`); + if (r.workflows > 0) parts.push(`${r.workflows} workflows`); + if (r.tasks > 0) parts.push(`${r.tasks} tasks`); + if (r.tools > 0) parts.push(`${r.tools} tools`); + detail = parts.join(', '); + } else if (handlerResult && handlerResult.counts) { + // Codex handler returns { success, counts: { agents, workflows, tasks }, written } + const c = handlerResult.counts; + const parts = []; + if (c.agents > 0) parts.push(`${c.agents} agents`); + if (c.workflows > 0) parts.push(`${c.workflows} workflows`); + if (c.tasks > 0) parts.push(`${c.tasks} tasks`); + detail = parts.join(', '); + } else if (handlerResult && handlerResult.modes !== undefined) { + // Kilo handler returns { success, modes, workflows, tasks, tools } + const parts = []; + if (handlerResult.modes > 0) parts.push(`${handlerResult.modes} modes`); + if (handlerResult.workflows > 0) parts.push(`${handlerResult.workflows} workflows`); + if (handlerResult.tasks > 0) parts.push(`${handlerResult.tasks} tasks`); + if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`); + detail = parts.join(', '); + } + return { success: true, ide: ideName, detail, handlerResult }; } catch (error) { - console.error(chalk.red(`Failed to setup ${ideName}:`), error.message); - return { success: false, error: error.message }; + await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`); + return { success: false, ide: ideName, error: error.message }; } } @@ -248,7 +282,7 @@ class IdeManager { const handler = this.handlers.get(ideName.toLowerCase()); if (!handler) { - console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported for custom agent installation`)); + await prompts.log.warn(`IDE '${ideName}' is not yet supported for custom agent installation`); continue; } @@ -260,7 +294,7 @@ class IdeManager { } } } catch (error) { - console.warn(chalk.yellow(`⚠️ Failed to install ${ideName} launcher: ${error.message}`)); + await prompts.log.warn(`Failed to install ${ideName} launcher: ${error.message}`); } } diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index fc51a9a9d..b907c60c5 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -101,9 +101,6 @@ platforms: - target_dir: .github/agents template_type: copilot_agents artifact_types: [agents] - - target_dir: .vscode - template_type: vscode_settings - artifact_types: [] iflow: name: "iFlow" @@ -134,8 +131,13 @@ platforms: category: ide description: "OpenCode terminal coding assistant" installer: - target_dir: .opencode/command - template_type: opencode + targets: + - target_dir: .opencode/agent + template_type: opencode + artifact_types: [agents] + - target_dir: .opencode/command + template_type: opencode + artifact_types: [workflows, tasks, tools] qwen: name: "QwenCoder" diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index dec22a12a..0915c306b 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -1,14 +1,13 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); -const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); +const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils'); /** * Generates launcher command files for each agent * Similar to WorkflowCommandGenerator but for agents */ class AgentCommandGenerator { - constructor(bmadFolderName = 'bmad') { + constructor(bmadFolderName = BMAD_FOLDER_NAME) { this.templatePath = path.join(__dirname, '../templates/agent-command-template.md'); this.bmadFolderName = bmadFolderName; } diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js index e88a64f5d..7bcfd6a79 100644 --- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -141,13 +141,24 @@ async function getTasksFromDir(dirPath, moduleName) { const files = await fs.readdir(dirPath); for (const file of files) { - if (!file.endsWith('.md')) { + // Include both .md and .xml task files + if (!file.endsWith('.md') && !file.endsWith('.xml')) { continue; } + const filePath = path.join(dirPath, file); + const content = await fs.readFile(filePath, 'utf8'); + + // Skip internal/engine files (not user-facing tasks) + if (content.includes('internal="true"')) { + continue; + } + + // Remove extension to get task name + const ext = file.endsWith('.xml') ? '.xml' : '.md'; tasks.push({ - path: path.join(dirPath, file), - name: file.replace('.md', ''), + path: filePath, + name: file.replace(ext, ''), module: moduleName, }); } diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index d6ad00f51..519669233 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -18,6 +18,9 @@ const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools']; const AGENT_SEGMENT = 'agents'; +// BMAD installation folder name - centralized constant for all installers +const BMAD_FOLDER_NAME = '_bmad'; + /** * Convert hierarchical path to flat dash-separated name (NEW STANDARD) * Converts: 'bmm', 'agents', 'pm' → 'bmad-agent-bmm-pm.md' @@ -59,7 +62,9 @@ function toDashPath(relativePath) { return 'bmad-unknown.md'; } - const withoutExt = relativePath.replace('.md', ''); + // Strip common file extensions to avoid double extensions in generated filenames + // e.g., 'create-story.xml' → 'create-story', 'workflow.yaml' → 'workflow' + const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, ''); const parts = withoutExt.split(/[/\\]/); const module = parts[0]; @@ -183,7 +188,8 @@ function toUnderscoreName(module, type, name) { * @deprecated Use toDashPath instead */ function toUnderscorePath(relativePath) { - const withoutExt = relativePath.replace('.md', ''); + // Strip common file extensions (same as toDashPath for consistency) + const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, ''); const parts = withoutExt.split(/[/\\]/); const module = parts[0]; @@ -289,4 +295,5 @@ module.exports = { TYPE_SEGMENTS, AGENT_SEGMENT, + BMAD_FOLDER_NAME, }; diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js index a0c4bcf87..ece1c8630 100644 --- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js @@ -1,13 +1,95 @@ const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); -const chalk = require('chalk'); -const { toColonName, toColonPath, toDashPath } = require('./path-utils'); +const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils'); /** * Generates command files for standalone tasks and tools */ class TaskToolCommandGenerator { + /** + * @param {string} bmadFolderName - Name of the BMAD folder for template rendering (default: '_bmad') + * Note: This parameter is accepted for API consistency with AgentCommandGenerator and + * WorkflowCommandGenerator, but is not used for path stripping. The manifest always stores + * filesystem paths with '_bmad/' prefix (the actual folder name), while bmadFolderName is + * used for template placeholder rendering ({{bmadFolderName}}). + */ + constructor(bmadFolderName = BMAD_FOLDER_NAME) { + this.bmadFolderName = bmadFolderName; + } + + /** + * Collect task and tool artifacts for IDE installation + * @param {string} bmadDir - BMAD installation directory + * @returns {Promise} Artifacts array with metadata + */ + async collectTaskToolArtifacts(bmadDir) { + const tasks = await this.loadTaskManifest(bmadDir); + const tools = await this.loadToolManifest(bmadDir); + + // All tasks/tools in manifest are standalone (internal=true items are filtered during manifest generation) + const artifacts = []; + const bmadPrefix = `${BMAD_FOLDER_NAME}/`; + + // Collect task artifacts + for (const task of tasks || []) { + let taskPath = (task.path || '').replaceAll('\\', '/'); + // Convert absolute paths to relative paths + if (path.isAbsolute(taskPath)) { + taskPath = path.relative(bmadDir, taskPath).replaceAll('\\', '/'); + } + // Remove _bmad/ prefix if present to get relative path within bmad folder + if (taskPath.startsWith(bmadPrefix)) { + taskPath = taskPath.slice(bmadPrefix.length); + } + + const taskExt = path.extname(taskPath) || '.md'; + artifacts.push({ + type: 'task', + name: task.name, + displayName: task.displayName || task.name, + description: task.description || `Execute ${task.displayName || task.name}`, + module: task.module, + // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) + relativePath: `${task.module}/tasks/${task.name}${taskExt}`, + path: taskPath, + }); + } + + // Collect tool artifacts + for (const tool of tools || []) { + let toolPath = (tool.path || '').replaceAll('\\', '/'); + // Convert absolute paths to relative paths + if (path.isAbsolute(toolPath)) { + toolPath = path.relative(bmadDir, toolPath).replaceAll('\\', '/'); + } + // Remove _bmad/ prefix if present to get relative path within bmad folder + if (toolPath.startsWith(bmadPrefix)) { + toolPath = toolPath.slice(bmadPrefix.length); + } + + const toolExt = path.extname(toolPath) || '.md'; + artifacts.push({ + type: 'tool', + name: tool.name, + displayName: tool.displayName || tool.name, + description: tool.description || `Execute ${tool.displayName || tool.name}`, + module: tool.module, + // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) + relativePath: `${tool.module}/tools/${tool.name}${toolExt}`, + path: toolPath, + }); + } + + return { + artifacts, + counts: { + tasks: (tasks || []).length, + tools: (tools || []).length, + }, + }; + } + /** * Generate task and tool commands from manifest CSVs * @param {string} projectDir - Project directory @@ -18,17 +100,13 @@ class TaskToolCommandGenerator { const tasks = await this.loadTaskManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir); - // Filter to only standalone items - const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - // Base commands directory - use provided or default to Claude Code structure const commandsDir = baseCommandsDir || path.join(projectDir, '.claude', 'commands', 'bmad'); let generatedCount = 0; // Generate command files for tasks - for (const task of standaloneTasks) { + for (const task of tasks || []) { const moduleTasksDir = path.join(commandsDir, task.module, 'tasks'); await fs.ensureDir(moduleTasksDir); @@ -40,7 +118,7 @@ class TaskToolCommandGenerator { } // Generate command files for tools - for (const tool of standaloneTools) { + for (const tool of tools || []) { const moduleToolsDir = path.join(commandsDir, tool.module, 'tools'); await fs.ensureDir(moduleToolsDir); @@ -53,8 +131,8 @@ class TaskToolCommandGenerator { return { generated: generatedCount, - tasks: standaloneTasks.length, - tools: standaloneTools.length, + tasks: (tasks || []).length, + tools: (tools || []).length, }; } @@ -65,9 +143,35 @@ class TaskToolCommandGenerator { const description = item.description || `Execute ${item.displayName || item.name}`; // Convert path to use {project-root} placeholder + // Handle undefined/missing path by constructing from module and name let itemPath = item.path; - if (itemPath && typeof itemPath === 'string' && itemPath.startsWith('bmad/')) { - itemPath = `{project-root}/${itemPath}`; + if (!itemPath || typeof itemPath !== 'string') { + // Fallback: construct path from module and name if path is missing + const typePlural = type === 'task' ? 'tasks' : 'tools'; + itemPath = `{project-root}/${this.bmadFolderName}/${item.module}/${typePlural}/${item.name}.md`; + } else { + // Normalize path separators to forward slashes + itemPath = itemPath.replaceAll('\\', '/'); + + // Extract relative path from absolute paths (Windows or Unix) + // Look for _bmad/ or bmad/ in the path and extract everything after it + // Match patterns like: /_bmad/core/tasks/... or /bmad/core/tasks/... + // Use [/\\] to handle both Unix forward slashes and Windows backslashes, + // and also paths without a leading separator (e.g., C:/_bmad/...) + const bmadMatch = itemPath.match(/[/\\]_bmad[/\\](.+)$/) || itemPath.match(/[/\\]bmad[/\\](.+)$/); + if (bmadMatch) { + // Found /_bmad/ or /bmad/ - use relative path after it + itemPath = `{project-root}/${this.bmadFolderName}/${bmadMatch[1]}`; + } else if (itemPath.startsWith(`${BMAD_FOLDER_NAME}/`)) { + // Relative path starting with _bmad/ + itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(BMAD_FOLDER_NAME.length + 1)}`; + } else if (itemPath.startsWith('bmad/')) { + // Relative path starting with bmad/ + itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(5)}`; + } else if (!itemPath.startsWith('{project-root}')) { + // For other relative paths, prefix with project root and bmad folder + itemPath = `{project-root}/${this.bmadFolderName}/${itemPath}`; + } } return `--- @@ -119,7 +223,7 @@ Follow all instructions in the ${type} file exactly as written. /** * Generate task and tool commands using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_bmad-help.md + * Creates flat files like: bmad_bmm_help.md * * @param {string} projectDir - Project directory * @param {string} bmadDir - BMAD installation directory @@ -130,14 +234,10 @@ Follow all instructions in the ${type} file exactly as written. const tasks = await this.loadTaskManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir); - // Filter to only standalone items - const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - let generatedCount = 0; // Generate command files for tasks - for (const task of standaloneTasks) { + for (const task of tasks || []) { const commandContent = this.generateCommandContent(task, 'task'); // Use underscore format: bmad_bmm_name.md const flatName = toColonName(task.module, 'tasks', task.name); @@ -148,7 +248,7 @@ Follow all instructions in the ${type} file exactly as written. } // Generate command files for tools - for (const tool of standaloneTools) { + for (const tool of tools || []) { const commandContent = this.generateCommandContent(tool, 'tool'); // Use underscore format: bmad_bmm_name.md const flatName = toColonName(tool.module, 'tools', tool.name); @@ -160,14 +260,14 @@ Follow all instructions in the ${type} file exactly as written. return { generated: generatedCount, - tasks: standaloneTasks.length, - tools: standaloneTools.length, + tasks: (tasks || []).length, + tools: (tools || []).length, }; } /** * Generate task and tool commands using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_bmad-help.md + * Creates flat files like: bmad_bmm_help.md * * @param {string} projectDir - Project directory * @param {string} bmadDir - BMAD installation directory @@ -178,16 +278,12 @@ Follow all instructions in the ${type} file exactly as written. const tasks = await this.loadTaskManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir); - // Filter to only standalone items - const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - let generatedCount = 0; // Generate command files for tasks - for (const task of standaloneTasks) { + for (const task of tasks || []) { const commandContent = this.generateCommandContent(task, 'task'); - // Use underscore format: bmad_bmm_name.md + // Use dash format: bmad-bmm-name.md const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); @@ -196,9 +292,9 @@ Follow all instructions in the ${type} file exactly as written. } // Generate command files for tools - for (const tool of standaloneTools) { + for (const tool of tools || []) { const commandContent = this.generateCommandContent(tool, 'tool'); - // Use underscore format: bmad_bmm_name.md + // Use dash format: bmad-bmm-name.md const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); @@ -208,14 +304,14 @@ Follow all instructions in the ${type} file exactly as written. return { generated: generatedCount, - tasks: standaloneTasks.length, - tools: standaloneTools.length, + tasks: (tasks || []).length, + tools: (tools || []).length, }; } /** * Write task/tool artifacts using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_bmad-help.md + * Creates flat files like: bmad_bmm_help.md * * @param {string} baseCommandsDir - Base commands directory for the IDE * @param {Array} artifacts - Task/tool artifacts with relativePath @@ -241,7 +337,7 @@ Follow all instructions in the ${type} file exactly as written. /** * Write task/tool artifacts using dash format (NEW STANDARD) - * Creates flat files like: bmad-bmm-bmad-help.md + * Creates flat files like: bmad-bmm-help.md * * Note: Tasks/tools do NOT have bmad-agent- prefix - only agents do. * diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js index 6dab1a3f2..d94e77db1 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -1,14 +1,14 @@ const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); -const chalk = require('chalk'); -const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); +const prompts = require('../../../../lib/prompts'); +const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils'); /** * Generates command files for each workflow in the manifest */ class WorkflowCommandGenerator { - constructor(bmadFolderName = 'bmad') { + constructor(bmadFolderName = BMAD_FOLDER_NAME) { this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md'); this.bmadFolderName = bmadFolderName; } @@ -22,7 +22,7 @@ class WorkflowCommandGenerator { const workflows = await this.loadWorkflowManifest(bmadDir); if (!workflows) { - console.log(chalk.yellow('Workflow manifest not found. Skipping command generation.')); + await prompts.log.warn('Workflow manifest not found. Skipping command generation.'); return { generated: 0 }; } @@ -157,8 +157,7 @@ class WorkflowCommandGenerator { .replaceAll('{{module}}', workflow.module) .replaceAll('{{description}}', workflow.description) .replaceAll('{{workflow_path}}', workflowPath) - .replaceAll('_bmad', this.bmadFolderName) - .replaceAll('_bmad', '_bmad'); + .replaceAll('_bmad', this.bmadFolderName); } /** @@ -238,15 +237,15 @@ When running any workflow: const match = workflowPath.match(/\/src\/bmm\/(.+)/); if (match) { transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`; - } else if (workflowPath.includes('/src/core/')) { - const match = workflowPath.match(/\/src\/core\/(.+)/); - if (match) { - transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`; - } } - - return transformed; + } else if (workflowPath.includes('/src/core/')) { + const match = workflowPath.match(/\/src\/core\/(.+)/); + if (match) { + transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`; + } } + + return transformed; } async loadWorkflowManifest(bmadDir) { diff --git a/tools/cli/installers/lib/ide/templates/combined/default-task.md b/tools/cli/installers/lib/ide/templates/combined/default-task.md new file mode 100644 index 000000000..b865d6ffb --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/default-task.md @@ -0,0 +1,10 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- + +# {{name}} + +Read the entire task file at: {project-root}/{{bmadFolderName}}/{{path}} + +Follow all instructions in the task file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/default-tool.md b/tools/cli/installers/lib/ide/templates/combined/default-tool.md new file mode 100644 index 000000000..11c6aac8d --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/default-tool.md @@ -0,0 +1,10 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- + +# {{name}} + +Read the entire tool file at: {project-root}/{{bmadFolderName}}/{{path}} + +Follow all instructions in the tool file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml b/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml new file mode 100644 index 000000000..7d15e2164 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml @@ -0,0 +1,11 @@ +description = "Executes the {{name}} task from the BMAD Method." +prompt = """ +Execute the BMAD '{{name}}' task. + +TASK INSTRUCTIONS: +1. LOAD the task file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents +3. FOLLOW every instruction precisely as specified + +TASK FILE: {project-root}/{{bmadFolderName}}/{{path}} +""" diff --git a/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml b/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml new file mode 100644 index 000000000..fc78c6b72 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml @@ -0,0 +1,11 @@ +description = "Executes the {{name}} tool from the BMAD Method." +prompt = """ +Execute the BMAD '{{name}}' tool. + +TOOL INSTRUCTIONS: +1. LOAD the tool file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents +3. FOLLOW every instruction precisely as specified + +TOOL FILE: {project-root}/{{bmadFolderName}}/{{path}} +""" diff --git a/tools/cli/installers/lib/ide/templates/split/opencode/body.md b/tools/cli/installers/lib/ide/templates/combined/opencode-agent.md similarity index 78% rename from tools/cli/installers/lib/ide/templates/split/opencode/body.md rename to tools/cli/installers/lib/ide/templates/combined/opencode-agent.md index b20f6651e..1102aa8a1 100644 --- a/tools/cli/installers/lib/ide/templates/split/opencode/body.md +++ b/tools/cli/installers/lib/ide/templates/combined/opencode-agent.md @@ -1,7 +1,12 @@ +--- +mode: primary +description: '{{description}}' +--- + You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. -1. LOAD the FULL agent file from {project-root}/_bmad/{{path}} +1. LOAD the FULL agent file from {project-root}/{{bmadFolderName}}/{{path}} 2. READ its entire contents - this contains the complete agent persona, menu, and instructions 3. FOLLOW every step in the section precisely 4. DISPLAY the welcome/greeting as instructed diff --git a/tools/cli/installers/lib/ide/templates/combined/opencode-task.md b/tools/cli/installers/lib/ide/templates/combined/opencode-task.md new file mode 100644 index 000000000..155c135c4 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/opencode-task.md @@ -0,0 +1,12 @@ +--- +description: '{{description}}' +--- + +Execute the BMAD '{{name}}' task. + +TASK INSTRUCTIONS: +1. LOAD the task file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents +3. FOLLOW every instruction precisely as specified + +TASK FILE: {project-root}/{{bmadFolderName}}/{{path}} diff --git a/tools/cli/installers/lib/ide/templates/combined/opencode-tool.md b/tools/cli/installers/lib/ide/templates/combined/opencode-tool.md new file mode 100644 index 000000000..505445253 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/opencode-tool.md @@ -0,0 +1,12 @@ +--- +description: '{{description}}' +--- + +Execute the BMAD '{{name}}' tool. + +TOOL INSTRUCTIONS: +1. LOAD the tool file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents +3. FOLLOW every instruction precisely as specified + +TOOL FILE: {project-root}/{{bmadFolderName}}/{{path}} diff --git a/tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md b/tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md new file mode 100644 index 000000000..d1e2b0af2 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md @@ -0,0 +1,15 @@ +--- +description: '{{description}}' +--- + +Execute the BMAD '{{name}}' workflow. + +CRITICAL: You must load and follow the workflow definition exactly. + +WORKFLOW INSTRUCTIONS: +1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents +3. FOLLOW every step precisely as specified +4. DO NOT skip or modify any steps + +WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}} diff --git a/tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md b/tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md new file mode 100644 index 000000000..d1e2b0af2 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md @@ -0,0 +1,15 @@ +--- +description: '{{description}}' +--- + +Execute the BMAD '{{name}}' workflow. + +CRITICAL: You must load and follow the workflow definition exactly. + +WORKFLOW INSTRUCTIONS: +1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents +3. FOLLOW every step precisely as specified +4. DO NOT skip or modify any steps + +WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}} diff --git a/tools/cli/installers/lib/ide/templates/split/.gitkeep b/tools/cli/installers/lib/ide/templates/split/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tools/cli/installers/lib/ide/templates/split/opencode/header.md b/tools/cli/installers/lib/ide/templates/split/opencode/header.md deleted file mode 100644 index a384374c4..000000000 --- a/tools/cli/installers/lib/ide/templates/split/opencode/header.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -name: '{{name}}' -description: '{{description}}' ---- diff --git a/tools/cli/installers/lib/message-loader.js b/tools/cli/installers/lib/message-loader.js index dd1126693..7198f0328 100644 --- a/tools/cli/installers/lib/message-loader.js +++ b/tools/cli/installers/lib/message-loader.js @@ -1,7 +1,7 @@ const fs = require('fs-extra'); const path = require('node:path'); const yaml = require('yaml'); -const chalk = require('chalk'); +const prompts = require('../../lib/prompts'); /** * Load and display installer messages from messages.yaml @@ -51,22 +51,20 @@ class MessageLoader { /** * Display the start message (after logo, before prompts) */ - displayStartMessage() { + async displayStartMessage() { const message = this.getStartMessage(); if (message) { - console.log(chalk.cyan(message)); - console.log(); + await prompts.log.info(message); } } /** * Display the end message (after installation completes) */ - displayEndMessage() { + async displayEndMessage() { const message = this.getEndMessage(); if (message) { - console.log(); - console.log(chalk.cyan(message)); + await prompts.log.info(message); } } diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index 60c087b19..0af4312fc 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -1,12 +1,12 @@ const path = require('node:path'); const fs = require('fs-extra'); const yaml = require('yaml'); -const chalk = require('chalk'); -const ora = require('ora'); +const prompts = require('../../../lib/prompts'); const { XmlHandler } = require('../../../lib/xml-handler'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { filterCustomizationData } = require('../../../lib/agent/compiler'); const { ExternalModuleManager } = require('./external-manager'); +const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); /** * Manages the installation, updating, and removal of BMAD modules. @@ -16,7 +16,7 @@ const { ExternalModuleManager } = require('./external-manager'); * @class ModuleManager * @requires fs-extra * @requires yaml - * @requires chalk + * @requires prompts * @requires XmlHandler * * @example @@ -27,7 +27,7 @@ const { ExternalModuleManager } = require('./external-manager'); class ModuleManager { constructor(options = {}) { this.xmlHandler = new XmlHandler(); - this.bmadFolderName = 'bmad'; // Default, can be overridden + this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden this.customModulePaths = new Map(); // Initialize custom module paths this.externalModuleManager = new ExternalModuleManager(); // For external official modules } @@ -151,26 +151,26 @@ class ModuleManager { // File hasn't been modified by user, safe to update await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Updated sidecar file: ${relativeToBmad}`)); + await prompts.log.message(` Updated sidecar file: ${relativeToBmad}`); } } else { // User has modified the file, preserve it if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Preserving user-modified file: ${relativeToBmad}`)); + await prompts.log.message(` Preserving user-modified file: ${relativeToBmad}`); } } } else { // First time seeing this file in manifest, copy it await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Added new sidecar file: ${relativeToBmad}`)); + await prompts.log.message(` Added new sidecar file: ${relativeToBmad}`); } } } else { // New installation await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Copied sidecar file: ${relativeToBmad}`)); + await prompts.log.message(` Copied sidecar file: ${relativeToBmad}`); } } @@ -287,7 +287,7 @@ class ModuleManager { moduleInfo.dependencies = config.dependencies || []; moduleInfo.defaultSelected = config.default_selected === undefined ? false : config.default_selected; } catch (error) { - console.warn(`Failed to read config for ${defaultName}:`, error.message); + await prompts.log.warn(`Failed to read config for ${defaultName}: ${error.message}`); } return moduleInfo; @@ -298,7 +298,7 @@ class ModuleManager { * @param {string} moduleCode - Code of the module to find (from module.yaml) * @returns {string|null} Path to the module source or null if not found */ - async findModuleSource(moduleCode) { + async findModuleSource(moduleCode, options = {}) { const projectRoot = getProjectRoot(); // First check custom module paths if they exist @@ -315,7 +315,7 @@ class ModuleManager { } // Check external official modules - const externalSource = await this.findExternalModuleSource(moduleCode); + const externalSource = await this.findExternalModuleSource(moduleCode, options); if (externalSource) { return externalSource; } @@ -347,7 +347,7 @@ class ModuleManager { * @param {string} moduleCode - Code of the external module * @returns {string} Path to the cloned repository */ - async cloneExternalModule(moduleCode) { + async cloneExternalModule(moduleCode, options = {}) { const { execSync } = require('node:child_process'); const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode); @@ -357,10 +357,32 @@ class ModuleManager { const cacheDir = this.getExternalCacheDir(); const moduleCacheDir = path.join(cacheDir, moduleCode); + const silent = options.silent || false; // Create cache directory if it doesn't exist await fs.ensureDir(cacheDir); + // Helper to create a spinner or a no-op when silent + const createSpinner = async () => { + if (silent) { + return { + start() {}, + stop() {}, + error() {}, + message() {}, + cancel() {}, + clear() {}, + get isSpinning() { + return false; + }, + get isCancelled() { + return false; + }, + }; + } + return await prompts.spinner(); + }; + // Track if we need to install dependencies let needsDependencyInstall = false; let wasNewClone = false; @@ -368,21 +390,30 @@ class ModuleManager { // Check if already cloned if (await fs.pathExists(moduleCacheDir)) { // Try to update if it's a git repo - const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start(); + const fetchSpinner = await createSpinner(); + fetchSpinner.start(`Fetching ${moduleInfo.name}...`); try { const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim(); // Fetch and reset to remote - works better with shallow clones than pull - execSync('git fetch origin --depth 1', { cwd: moduleCacheDir, stdio: 'pipe' }); - execSync('git reset --hard origin/HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }); + execSync('git fetch origin --depth 1', { + cwd: moduleCacheDir, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, + }); + execSync('git reset --hard origin/HEAD', { + cwd: moduleCacheDir, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, + }); const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim(); - fetchSpinner.succeed(`Fetched ${moduleInfo.name}`); + fetchSpinner.stop(`Fetched ${moduleInfo.name}`); // Force dependency install if we got new code if (currentRef !== newRef) { needsDependencyInstall = true; } } catch { - fetchSpinner.warn(`Fetch failed, re-downloading ${moduleInfo.name}`); + fetchSpinner.error(`Fetch failed, re-downloading ${moduleInfo.name}`); // If update fails, remove and re-clone await fs.remove(moduleCacheDir); wasNewClone = true; @@ -393,14 +424,16 @@ class ModuleManager { // Clone if not exists or was removed if (wasNewClone) { - const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start(); + const fetchSpinner = await createSpinner(); + fetchSpinner.start(`Fetching ${moduleInfo.name}...`); try { execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, { - stdio: 'pipe', + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, }); - fetchSpinner.succeed(`Fetched ${moduleInfo.name}`); + fetchSpinner.stop(`Fetched ${moduleInfo.name}`); } catch (error) { - fetchSpinner.fail(`Failed to fetch ${moduleInfo.name}`); + fetchSpinner.error(`Failed to fetch ${moduleInfo.name}`); throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`); } } @@ -414,17 +447,18 @@ class ModuleManager { // Force install if we updated or cloned new if (needsDependencyInstall || wasNewClone || nodeModulesMissing) { - const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start(); + const installSpinner = await createSpinner(); + installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`); try { - execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress --legacy-peer-deps', { + execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { cwd: moduleCacheDir, - stdio: 'pipe', + stdio: ['ignore', 'pipe', 'pipe'], timeout: 120_000, // 2 minute timeout }); - installSpinner.succeed(`Installed dependencies for ${moduleInfo.name}`); + installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`); } catch (error) { - installSpinner.warn(`Failed to install dependencies for ${moduleInfo.name}`); - console.warn(chalk.yellow(` Warning: ${error.message}`)); + installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`); + if (!silent) await prompts.log.warn(` Warning: ${error.message}`); } } else { // Check if package.json is newer than node_modules @@ -439,17 +473,18 @@ class ModuleManager { } if (packageJsonNewer) { - const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start(); + const installSpinner = await createSpinner(); + installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`); try { - execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress --legacy-peer-deps', { + execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { cwd: moduleCacheDir, - stdio: 'pipe', + stdio: ['ignore', 'pipe', 'pipe'], timeout: 120_000, // 2 minute timeout }); - installSpinner.succeed(`Installed dependencies for ${moduleInfo.name}`); + installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`); } catch (error) { - installSpinner.warn(`Failed to install dependencies for ${moduleInfo.name}`); - console.warn(chalk.yellow(` Warning: ${error.message}`)); + installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`); + if (!silent) await prompts.log.warn(` Warning: ${error.message}`); } } } @@ -463,7 +498,7 @@ class ModuleManager { * @param {string} moduleCode - Code of the external module * @returns {string|null} Path to the module source or null if not found */ - async findExternalModuleSource(moduleCode) { + async findExternalModuleSource(moduleCode, options = {}) { const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode); if (!moduleInfo) { @@ -471,7 +506,7 @@ class ModuleManager { } // Clone the external module repo - const cloneDir = await this.cloneExternalModule(moduleCode); + const cloneDir = await this.cloneExternalModule(moduleCode, options); // The module-definition specifies the path to module.yaml relative to repo root // We need to return the directory containing module.yaml @@ -492,7 +527,7 @@ class ModuleManager { * @param {Object} options.logger - Logger instance for output */ async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) { - const sourcePath = await this.findModuleSource(moduleName); + const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); const targetPath = path.join(bmadDir, moduleName); // Check if source module exists @@ -513,14 +548,14 @@ class ModuleManager { const customContent = await fs.readFile(rootCustomConfigPath, 'utf8'); customConfig = yaml.parse(customContent); } catch (error) { - console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message)); + await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`); } } else if (await fs.pathExists(moduleInstallerCustomPath)) { try { const customContent = await fs.readFile(moduleInstallerCustomPath, 'utf8'); customConfig = yaml.parse(customContent); } catch (error) { - console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message)); + await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`); } } @@ -528,7 +563,7 @@ class ModuleManager { if (customConfig) { options.moduleConfig = { ...options.moduleConfig, ...customConfig }; if (options.logger) { - options.logger.log(chalk.cyan(` Merged custom configuration for ${moduleName}`)); + options.logger.log(` Merged custom configuration for ${moduleName}`); } } @@ -581,7 +616,7 @@ class ModuleManager { * @param {string} bmadDir - Target bmad directory * @param {boolean} force - Force update (overwrite modifications) */ - async update(moduleName, bmadDir, force = false) { + async update(moduleName, bmadDir, force = false, options = {}) { const sourcePath = await this.findModuleSource(moduleName); const targetPath = path.join(bmadDir, moduleName); @@ -598,7 +633,7 @@ class ModuleManager { if (force) { // Force update - remove and reinstall await fs.remove(targetPath); - return await this.install(moduleName, bmadDir); + return await this.install(moduleName, bmadDir, null, { installer: options.installer }); } else { // Selective update - preserve user modifications await this.syncModule(sourcePath, targetPath); @@ -672,7 +707,7 @@ class ModuleManager { const config = yaml.parse(configContent); Object.assign(moduleInfo, config); } catch (error) { - console.warn(`Failed to read installed module config:`, error.message); + await prompts.log.warn(`Failed to read installed module config: ${error.message}`); } } @@ -734,7 +769,7 @@ class ModuleManager { // Check for localskip="true" in the agent tag const agentMatch = content.match(/]*\slocalskip="true"[^>]*>/); if (agentMatch) { - console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`)); + await prompts.log.message(` Skipping web-only agent: ${path.basename(file)}`); continue; // Skip this agent } } @@ -767,7 +802,6 @@ class ModuleManager { // IMPORTANT: Replace escape sequence and placeholder BEFORE parsing YAML // Otherwise parsing will fail on the placeholder - yamlContent = yamlContent.replaceAll('_bmad', '_bmad'); yamlContent = yamlContent.replaceAll('_bmad', this.bmadFolderName); try { @@ -780,10 +814,6 @@ class ModuleManager { return; } - // Remove web_bundle section using regex to preserve formatting - // Match the web_bundle key and all its content (including nested items) - // This handles both web_bundle: false and web_bundle: {...} - // Find the line that starts web_bundle const lines = yamlContent.split('\n'); let startIdx = -1; @@ -841,7 +871,7 @@ class ModuleManager { await fs.writeFile(targetFile, strippedYaml, 'utf8'); } catch { // If anything fails, just copy the file as-is - console.warn(chalk.yellow(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`)); + await prompts.log.warn(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`); await fs.copy(sourceFile, targetFile, { overwrite: true }); } } @@ -870,7 +900,7 @@ class ModuleManager { for (const agentFile of agentFiles) { if (!agentFile.endsWith('.agent.yaml')) continue; - const relativePath = path.relative(sourceAgentsPath, agentFile); + const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/'); const targetDir = path.join(targetAgentsPath, path.dirname(relativePath)); await fs.ensureDir(targetDir); @@ -893,7 +923,7 @@ class ModuleManager { await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath); // Only show customize creation in verbose mode if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`)); + await prompts.log.message(` Created customize: ${moduleName}-${agentName}.customize.yaml`); } // Store original hash for modification detection @@ -993,10 +1023,10 @@ class ModuleManager { const copiedFiles = await this.copySidecarToMemory(sourceSidecarPath, agentName, bmadMemoryPath, isUpdate, bmadDir, installer); if (process.env.BMAD_VERBOSE_INSTALL === 'true' && copiedFiles.length > 0) { - console.log(chalk.dim(` Sidecar files processed: ${copiedFiles.length} files`)); + await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`); } } else if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.yellow(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`)); + await prompts.log.warn(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`); } } @@ -1015,14 +1045,12 @@ class ModuleManager { // Only show compilation details in verbose mode if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log( - chalk.dim( - ` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`, - ), + await prompts.log.message( + ` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`, ); } } catch (error) { - console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message)); + await prompts.log.warn(` Failed to compile agent ${agentName}: ${error.message}`); } } } @@ -1142,11 +1170,11 @@ class ModuleManager { } if (!workflowsVendored) { - console.log(chalk.cyan(`\n Vendoring cross-module workflows for ${moduleName}...`)); + await prompts.log.info(`\n Vendoring cross-module workflows for ${moduleName}...`); workflowsVendored = true; } - console.log(chalk.dim(` Processing: ${agentFile}`)); + await prompts.log.message(` Processing: ${agentFile}`); for (const item of workflowInstallItems) { const sourceWorkflowPath = item.workflow; // Where to copy FROM @@ -1158,7 +1186,7 @@ class ModuleManager { // Or: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/); if (!sourceMatch) { - console.warn(chalk.yellow(` Could not parse workflow path: ${sourceWorkflowPath}`)); + await prompts.log.warn(` Could not parse workflow path: ${sourceWorkflowPath}`); continue; } @@ -1169,7 +1197,7 @@ class ModuleManager { // Example: {project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.yaml const installMatch = installWorkflowPath.match(/\{project-root\}\/(_bmad)\/([^/]+)\/workflows\/(.+)/); if (!installMatch) { - console.warn(chalk.yellow(` Could not parse workflow-install path: ${installWorkflowPath}`)); + await prompts.log.warn(` Could not parse workflow-install path: ${installWorkflowPath}`); continue; } @@ -1182,15 +1210,13 @@ class ModuleManager { // Check if source workflow exists if (!(await fs.pathExists(actualSourceWorkflowPath))) { - console.warn(chalk.yellow(` Source workflow not found: ${actualSourceWorkflowPath}`)); + await prompts.log.warn(` Source workflow not found: ${actualSourceWorkflowPath}`); continue; } // Copy the entire workflow folder - console.log( - chalk.dim( - ` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')} → ${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`, - ), + await prompts.log.message( + ` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')} → ${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`, ); await fs.ensureDir(path.dirname(actualDestWorkflowPath)); @@ -1206,7 +1232,7 @@ class ModuleManager { } if (workflowsVendored) { - console.log(chalk.green(` ✓ Workflow vendoring complete\n`)); + await prompts.log.success(` Workflow vendoring complete\n`); } } @@ -1228,7 +1254,7 @@ class ModuleManager { if (updatedYaml !== yamlContent) { await fs.writeFile(workflowYamlPath, updatedYaml, 'utf8'); - console.log(chalk.dim(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`)); + await prompts.log.message(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`); } } @@ -1244,7 +1270,7 @@ class ModuleManager { if (moduleName === 'core') { sourcePath = getSourcePath('core'); } else { - sourcePath = await this.findModuleSource(moduleName); + sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); if (!sourcePath) { // No source found, skip module installer return; @@ -1283,11 +1309,11 @@ class ModuleManager { }); if (!result) { - console.warn(chalk.yellow(`Module installer for ${moduleName} returned false`)); + await prompts.log.warn(`Module installer for ${moduleName} returned false`); } } } catch (error) { - console.error(chalk.red(`Error running module installer for ${moduleName}: ${error.message}`)); + await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`); } } @@ -1309,7 +1335,7 @@ class ModuleManager { await fs.writeFile(configPath, configContent, 'utf8'); } catch (error) { - console.warn(`Failed to process module config:`, error.message); + await prompts.log.warn(`Failed to process module config: ${error.message}`); } } } diff --git a/tools/cli/lib/agent/installer.js b/tools/cli/lib/agent/installer.js index b55502edd..c9e0dd916 100644 --- a/tools/cli/lib/agent/installer.js +++ b/tools/cli/lib/agent/installer.js @@ -6,7 +6,7 @@ const fs = require('node:fs'); const path = require('node:path'); const yaml = require('yaml'); -const readline = require('node:readline'); +const prompts = require('../prompts'); const { compileAgent, compileAgentFile } = require('./compiler'); const { extractInstallConfig, getDefaultValues } = require('./template-engine'); @@ -42,7 +42,7 @@ function findBmadConfig(startPath = process.cwd()) { * @returns {string} Resolved path */ function resolvePath(pathStr, context) { - return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context_bmadFolder); + return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context.bmadFolder); } /** @@ -149,83 +149,47 @@ async function promptInstallQuestions(installConfig, defaults, presetAnswers = { return { ...defaults, ...presetAnswers }; } - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - const question = (prompt) => - new Promise((resolve) => { - rl.question(prompt, resolve); - }); - const answers = { ...defaults, ...presetAnswers }; - console.log('\n📝 Agent Configuration\n'); - if (installConfig.description) { - console.log(` ${installConfig.description}\n`); - } + await prompts.note(installConfig.description || '', 'Agent Configuration'); for (const q of installConfig.questions) { // Skip questions for variables that are already set (e.g., custom_name set upfront) if (answers[q.var] !== undefined && answers[q.var] !== defaults[q.var]) { - console.log(chalk.dim(` ${q.var}: ${answers[q.var]} (already set)`)); + await prompts.log.message(` ${q.var}: ${answers[q.var]} (already set)`); continue; } - let response; - switch (q.type) { case 'text': { - const defaultHint = q.default ? ` (default: ${q.default})` : ''; - response = await question(` ${q.prompt}${defaultHint}: `); - answers[q.var] = response || q.default || ''; - + const response = await prompts.text({ + message: q.prompt, + default: q.default ?? '', + }); + answers[q.var] = response ?? q.default ?? ''; break; } case 'boolean': { - const defaultHint = q.default ? ' [Y/n]' : ' [y/N]'; - response = await question(` ${q.prompt}${defaultHint}: `); - if (response === '') { - answers[q.var] = q.default; - } else { - answers[q.var] = response.toLowerCase().startsWith('y'); - } - + const response = await prompts.confirm({ + message: q.prompt, + default: q.default, + }); + answers[q.var] = response; break; } case 'choice': { - console.log(` ${q.prompt}`); - for (const [idx, opt] of q.options.entries()) { - const marker = opt.value === q.default ? '* ' : ' '; - console.log(` ${marker}${idx + 1}. ${opt.label}`); - } - const defaultIdx = q.options.findIndex((o) => o.value === q.default) + 1; - let validChoice = false; - let choiceIdx; - while (!validChoice) { - response = await question(` Choice (default: ${defaultIdx}): `); - if (response) { - choiceIdx = parseInt(response, 10) - 1; - if (isNaN(choiceIdx) || choiceIdx < 0 || choiceIdx >= q.options.length) { - console.log(` Invalid choice. Please enter 1-${q.options.length}`); - } else { - validChoice = true; - } - } else { - choiceIdx = defaultIdx - 1; - validChoice = true; - } - } - answers[q.var] = q.options[choiceIdx].value; - + const response = await prompts.select({ + message: q.prompt, + options: q.options.map((o) => ({ value: o.value, label: o.label })), + initialValue: q.default, + }); + answers[q.var] = response; break; } // No default } } - rl.close(); return answers; } diff --git a/tools/cli/lib/cli-utils.js b/tools/cli/lib/cli-utils.js index da1933631..569f1c44c 100644 --- a/tools/cli/lib/cli-utils.js +++ b/tools/cli/lib/cli-utils.js @@ -1,9 +1,6 @@ -const chalk = require('chalk'); -const boxen = require('boxen'); -const wrapAnsi = require('wrap-ansi'); -const figlet = require('figlet'); const path = require('node:path'); const os = require('node:os'); +const prompts = require('./prompts'); const CLIUtils = { /** @@ -19,27 +16,32 @@ const CLIUtils = { }, /** - * Display BMAD logo - * @param {boolean} clearScreen - Whether to clear the screen first (default: true for initial display only) + * Display BMAD logo using @clack intro + box + * @param {boolean} _clearScreen - Deprecated, ignored (no longer clears screen) */ - displayLogo(clearScreen = true) { - if (clearScreen) { - console.clear(); - } - + async displayLogo(_clearScreen = true) { const version = this.getVersion(); + const color = await prompts.getColor(); // ASCII art logo - const logo = ` - ██████╗ ███╗ ███╗ █████╗ ██████╗ ™ - ██╔══██╗████╗ ████║██╔══██╗██╔══██╗ - ██████╔╝██╔████╔██║███████║██║ ██║ - ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║ - ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ - ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝`; + const logo = [ + ' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™', + ' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗', + ' ██████╔╝██╔████╔██║███████║██║ ██║', + ' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║', + ' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝', + ' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝', + ] + .map((line) => color.yellow(line)) + .join('\n'); - console.log(chalk.cyan(logo)); - console.log(chalk.dim(` Build More, Architect Dreams`) + chalk.cyan.bold(` v${version}`) + '\n'); + const tagline = ' Build More, Architect Dreams'; + + await prompts.box(`${logo}\n${tagline}`, `v${version}`, { + contentAlign: 'center', + rounded: true, + formatBorder: color.blue, + }); }, /** @@ -47,13 +49,8 @@ const CLIUtils = { * @param {string} title - Section title * @param {string} subtitle - Optional subtitle */ - displaySection(title, subtitle = null) { - console.log('\n' + chalk.cyan('═'.repeat(80))); - console.log(chalk.cyan.bold(` ${title}`)); - if (subtitle) { - console.log(chalk.dim(` ${subtitle}`)); - } - console.log(chalk.cyan('═'.repeat(80)) + '\n'); + async displaySection(title, subtitle = null) { + await prompts.note(subtitle || '', title); }, /** @@ -61,25 +58,21 @@ const CLIUtils = { * @param {string|Array} content - Content to display * @param {Object} options - Box options */ - displayBox(content, options = {}) { - const defaultOptions = { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'cyan', - ...options, - }; - - // Handle array content + async displayBox(content, options = {}) { let text = content; if (Array.isArray(content)) { text = content.join('\n\n'); } - // Wrap text to prevent overflow - const wrapped = wrapAnsi(text, 76, { hard: true, wordWrap: true }); + const color = await prompts.getColor(); + const borderColor = options.borderColor || 'cyan'; + const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue }; + const formatBorder = colorMap[borderColor] || color.cyan; - console.log(boxen(wrapped, defaultOptions)); + await prompts.box(text, options.title, { + rounded: options.borderStyle === 'round' || options.borderStyle === undefined, + formatBorder, + }); }, /** @@ -88,14 +81,9 @@ const CLIUtils = { * @param {string} header - Custom header from module.yaml * @param {string} subheader - Custom subheader from module.yaml */ - displayModuleConfigHeader(moduleName, header = null, subheader = null) { - // Simple blue banner with custom header/subheader if provided - console.log('\n' + chalk.cyan('─'.repeat(80))); - console.log(chalk.cyan(header || `Configuring ${moduleName.toUpperCase()} Module`)); - if (subheader) { - console.log(chalk.dim(`${subheader}`)); - } - console.log(chalk.cyan('─'.repeat(80)) + '\n'); + async displayModuleConfigHeader(moduleName, header = null, subheader = null) { + const title = header || `Configuring ${moduleName.toUpperCase()} Module`; + await prompts.note(subheader || '', title); }, /** @@ -104,14 +92,9 @@ const CLIUtils = { * @param {string} header - Custom header from module.yaml * @param {string} subheader - Custom subheader from module.yaml */ - displayModuleNoConfig(moduleName, header = null, subheader = null) { - // Show full banner with header/subheader, just like modules with config - console.log('\n' + chalk.cyan('─'.repeat(80))); - console.log(chalk.cyan(header || `${moduleName.toUpperCase()} Module - No Custom Configuration`)); - if (subheader) { - console.log(chalk.dim(`${subheader}`)); - } - console.log(chalk.cyan('─'.repeat(80)) + '\n'); + async displayModuleNoConfig(moduleName, header = null, subheader = null) { + const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`; + await prompts.note(subheader || '', title); }, /** @@ -120,42 +103,33 @@ const CLIUtils = { * @param {number} total - Total steps * @param {string} description - Step description */ - displayStep(current, total, description) { + async displayStep(current, total, description) { const progress = `[${current}/${total}]`; - console.log('\n' + chalk.cyan(progress) + ' ' + chalk.bold(description)); - console.log(chalk.dim('─'.repeat(80 - progress.length - 1)) + '\n'); + await prompts.log.step(`${progress} ${description}`); }, /** * Display completion message * @param {string} message - Completion message */ - displayComplete(message) { - console.log( - '\n' + - boxen(chalk.green('✨ ' + message), { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'green', - }), - ); + async displayComplete(message) { + const color = await prompts.getColor(); + await prompts.box(`\u2728 ${message}`, 'Complete', { + rounded: true, + formatBorder: color.green, + }); }, /** * Display error message * @param {string} message - Error message */ - displayError(message) { - console.log( - '\n' + - boxen(chalk.red('✗ ' + message), { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'red', - }), - ); + async displayError(message) { + const color = await prompts.getColor(); + await prompts.box(`\u2717 ${message}`, 'Error', { + rounded: true, + formatBorder: color.red, + }); }, /** @@ -163,7 +137,7 @@ const CLIUtils = { * @param {Array} items - Items to display * @param {string} prefix - Item prefix */ - formatList(items, prefix = '•') { + formatList(items, prefix = '\u2022') { return items.map((item) => ` ${prefix} ${item}`).join('\n'); }, @@ -178,25 +152,6 @@ const CLIUtils = { } }, - /** - * Display table - * @param {Array} data - Table data - * @param {Object} options - Table options - */ - displayTable(data, options = {}) { - const Table = require('cli-table3'); - const table = new Table({ - style: { - head: ['cyan'], - border: ['dim'], - }, - ...options, - }); - - for (const row of data) table.push(row); - console.log(table.toString()); - }, - /** * Display module completion message * @param {string} moduleName - Name of the completed module diff --git a/tools/cli/lib/prompts.js b/tools/cli/lib/prompts.js index 5d85e2b44..24500700b 100644 --- a/tools/cli/lib/prompts.js +++ b/tools/cli/lib/prompts.js @@ -8,6 +8,8 @@ */ let _clack = null; +let _clackCore = null; +let _picocolors = null; /** * Lazy-load @clack/prompts (ESM module) @@ -20,6 +22,28 @@ async function getClack() { return _clack; } +/** + * Lazy-load @clack/core (ESM module) + * @returns {Promise} The clack core module + */ +async function getClackCore() { + if (!_clackCore) { + _clackCore = await import('@clack/core'); + } + return _clackCore; +} + +/** + * Lazy-load picocolors + * @returns {Promise} The picocolors module + */ +async function getPicocolors() { + if (!_picocolors) { + _picocolors = (await import('picocolors')).default; + } + return _picocolors; +} + /** * Handle user cancellation gracefully * @param {any} value - The value to check @@ -65,11 +89,51 @@ async function note(message, title) { /** * Display a spinner for async operations - * @returns {Object} Spinner controller with start, stop, message methods + * Wraps @clack/prompts spinner with isSpinning state tracking + * @returns {Object} Spinner controller with start, stop, message, error, cancel, clear, isSpinning */ async function spinner() { const clack = await getClack(); - return clack.spinner(); + const s = clack.spinner(); + let spinning = false; + + return { + start: (msg) => { + if (spinning) { + s.message(msg); + } else { + spinning = true; + s.start(msg); + } + }, + stop: (msg) => { + if (spinning) { + spinning = false; + s.stop(msg); + } + }, + message: (msg) => { + if (spinning) s.message(msg); + }, + error: (msg) => { + spinning = false; + s.error(msg); + }, + cancel: (msg) => { + spinning = false; + s.cancel(msg); + }, + clear: () => { + spinning = false; + s.clear(); + }, + get isSpinning() { + return spinning; + }, + get isCancelled() { + return s.isCancelled; + }, + }; } /** @@ -167,26 +231,155 @@ async function multiselect(options) { } /** - * Grouped multi-select prompt for categorized options + * Default filter function for autocomplete - case-insensitive label matching + * @param {string} search - Search string + * @param {Object} option - Option object with label + * @returns {boolean} Whether the option matches + */ +function defaultAutocompleteFilter(search, option) { + const label = option.label ?? String(option.value ?? ''); + return label.toLowerCase().includes(search.toLowerCase()); +} + +/** + * Autocomplete multi-select prompt with type-ahead filtering + * Custom implementation that always shows "Space/Tab:" in the hint * @param {Object} options - Prompt options * @param {string} options.message - The question to ask - * @param {Object} options.options - Object mapping group names to arrays of choices + * @param {Array} options.options - Array of choices [{label, value, hint?}] + * @param {string} [options.placeholder] - Placeholder text for search input * @param {Array} [options.initialValues] - Array of initially selected values * @param {boolean} [options.required=false] - Whether at least one must be selected - * @param {boolean} [options.selectableGroups=false] - Whether groups can be selected as a whole + * @param {number} [options.maxItems=5] - Maximum visible items in scrollable list + * @param {Function} [options.filter] - Custom filter function (search, option) => boolean + * @param {Array} [options.lockedValues] - Values that are always selected and cannot be toggled off * @returns {Promise} Array of selected values */ -async function groupMultiselect(options) { +async function autocompleteMultiselect(options) { + const core = await getClackCore(); const clack = await getClack(); + const color = await getPicocolors(); - const result = await clack.groupMultiselect({ - message: options.message, + const filterFn = options.filter ?? defaultAutocompleteFilter; + const lockedSet = new Set(options.lockedValues || []); + + const prompt = new core.AutocompletePrompt({ options: options.options, - initialValues: options.initialValues, - required: options.required || false, - selectableGroups: options.selectableGroups || false, + multiple: true, + filter: filterFn, + validate: () => { + if (options.required && prompt.selectedValues.length === 0) { + return 'Please select at least one item'; + } + }, + initialValue: [...new Set([...(options.initialValues || []), ...(options.lockedValues || [])])], + render() { + const barColor = this.state === 'error' ? color.yellow : color.cyan; + const bar = barColor(clack.S_BAR); + const barEnd = barColor(clack.S_BAR_END); + + const title = `${color.gray(clack.S_BAR)}\n${clack.symbol(this.state)} ${options.message}\n`; + + const userInput = this.userInput; + const placeholder = options.placeholder || 'Type to search...'; + const hasPlaceholder = userInput === '' && placeholder !== undefined; + + // Show placeholder or user input with cursor + const searchDisplay = + this.isNavigating || hasPlaceholder ? color.dim(hasPlaceholder ? placeholder : userInput) : this.userInputWithCursor; + + const allOptions = this.options; + const matchCount = + this.filteredOptions.length === allOptions.length + ? '' + : color.dim(` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`); + + // Render option with checkbox + const renderOption = (opt, isHighlighted) => { + const isSelected = this.selectedValues.includes(opt.value); + const isLocked = lockedSet.has(opt.value); + const label = opt.label ?? String(opt.value ?? ''); + const hintText = opt.hint && isHighlighted ? color.dim(` (${opt.hint})`) : ''; + + let checkbox; + if (isLocked) { + checkbox = color.green(clack.S_CHECKBOX_SELECTED); + const lockHint = color.dim(' (always installed)'); + return isHighlighted ? `${checkbox} ${label}${lockHint}` : `${checkbox} ${color.dim(label)}${lockHint}`; + } + checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE); + return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`; + }; + + switch (this.state) { + case 'submit': { + return `${title}${color.gray(clack.S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`; + } + + case 'cancel': { + return `${title}${color.gray(clack.S_BAR)} ${color.strikethrough(color.dim(userInput))}`; + } + + default: { + // Always show "SPACE:" regardless of isNavigating state + const hints = [`${color.dim('↑/↓')} to navigate`, `${color.dim('TAB/SPACE:')} select`, `${color.dim('ENTER:')} confirm`]; + + const noMatchesLine = this.filteredOptions.length === 0 && userInput ? [`${bar} ${color.yellow('No matches found')}`] : []; + + const errorLine = this.state === 'error' ? [`${bar} ${color.yellow(this.error)}`] : []; + + const headerLines = [...`${title}${bar}`.split('\n'), `${bar} ${searchDisplay}${matchCount}`, ...noMatchesLine, ...errorLine]; + + const footerLines = [`${bar} ${color.dim(hints.join(' • '))}`, `${barEnd}`]; + + const optionLines = clack.limitOptions({ + cursor: this.cursor, + options: this.filteredOptions, + style: renderOption, + maxItems: options.maxItems || 5, + output: options.output, + rowPadding: headerLines.length + footerLines.length, + }); + + return [...headerLines, ...optionLines.map((line) => `${bar} ${line}`), ...footerLines].join('\n'); + } + } + }, }); + // Prevent locked values from being toggled off + if (lockedSet.size > 0) { + const originalToggle = prompt.toggleSelected.bind(prompt); + prompt.toggleSelected = function (value) { + // If locked and already selected, skip the toggle (would deselect) + if (lockedSet.has(value) && this.selectedValues.includes(value)) { + return; + } + originalToggle(value); + }; + } + + // === FIX: Make SPACE always act as selection key (not search input) === + // Override _isActionKey to treat SPACE like TAB - always an action key + // This prevents SPACE from being added to the search input + const originalIsActionKey = prompt._isActionKey.bind(prompt); + prompt._isActionKey = function (char, key) { + if (key && key.name === 'space') { + return true; + } + return originalIsActionKey(char, key); + }; + + // Handle SPACE toggle when NOT navigating (internal code only handles it when isNavigating=true) + prompt.on('key', (char, key) => { + if (key && key.name === 'space' && !prompt.isNavigating) { + const focused = prompt.filteredOptions[prompt.cursor]; + if (focused) prompt.toggleSelected(focused.value); + } + }); + // === END FIX === + + const result = await prompt.prompt(); await handleCancel(result); return result; } @@ -211,7 +404,12 @@ async function confirm(options) { } /** - * Text input prompt (replaces Inquirer 'input' type) + * Text input prompt with Tab-to-fill-placeholder support (replaces Inquirer 'input' type) + * + * This custom implementation restores the Tab-to-fill-placeholder behavior that was + * intentionally removed in @clack/prompts v1.0.0 (placeholder became purely visual). + * Uses @clack/core's TextPrompt primitive with custom key handling. + * * @param {Object} options - Prompt options * @param {string} options.message - The question to ask * @param {string} [options.default] - Default value @@ -220,20 +418,64 @@ async function confirm(options) { * @returns {Promise} User's input */ async function text(options) { - const clack = await getClack(); + const core = await getClackCore(); + const color = await getPicocolors(); // Use default as placeholder if placeholder not explicitly provided // This shows the default value as grayed-out hint text const placeholder = options.placeholder === undefined ? options.default : options.placeholder; + const defaultValue = options.default; - const result = await clack.text({ - message: options.message, - defaultValue: options.default, - placeholder: typeof placeholder === 'string' ? placeholder : undefined, + const prompt = new core.TextPrompt({ + defaultValue, validate: options.validate, + render() { + const title = `${color.gray('◆')} ${options.message}`; + + // Show placeholder as dim text when input is empty + let valueDisplay; + if (this.state === 'error') { + valueDisplay = color.yellow(this.userInputWithCursor); + } else if (this.userInput) { + valueDisplay = this.userInputWithCursor; + } else if (placeholder) { + // Show placeholder with cursor indicator when empty + valueDisplay = `${color.inverse(color.hidden('_'))}${color.dim(placeholder)}`; + } else { + valueDisplay = color.inverse(color.hidden('_')); + } + + const bar = color.gray('│'); + + // Handle different states + if (this.state === 'submit') { + return `${color.gray('◇')} ${options.message}\n${bar} ${color.dim(this.value || defaultValue || '')}`; + } + + if (this.state === 'cancel') { + return `${color.gray('◇')} ${options.message}\n${bar} ${color.strikethrough(color.dim(this.userInput || ''))}`; + } + + if (this.state === 'error') { + return `${color.yellow('▲')} ${options.message}\n${bar} ${valueDisplay}\n${color.yellow('│')} ${color.yellow(this.error)}`; + } + + return `${title}\n${bar} ${valueDisplay}\n${bar}`; + }, }); + // Add Tab key handler to fill placeholder into input + prompt.on('key', (char) => { + if (char === '\t' && placeholder && !prompt.userInput) { + // Use _setUserInput with write=true to populate the readline and update internal state + prompt._setUserInput(placeholder, true); + } + }); + + const result = await prompt.prompt(); await handleCancel(result); + + // TextPrompt's finalize handler already applies defaultValue for empty input return result; } @@ -316,6 +558,131 @@ const log = { }, }; +/** + * Display cancellation message + * @param {string} [message='Operation cancelled'] - The cancellation message + */ +async function cancel(message = 'Operation cancelled') { + const clack = await getClack(); + clack.cancel(message); +} + +/** + * Display content in a styled box + * @param {string} content - The box content + * @param {string} [title] - Optional title + * @param {Object} [options] - Box options (contentAlign, titleAlign, width, rounded, formatBorder, etc.) + */ +async function box(content, title, options) { + const clack = await getClack(); + clack.box(content, title, options); +} + +/** + * Create a progress bar for visualizing task completion + * @param {Object} [options] - Progress options (max, style, etc.) + * @returns {Promise} Progress controller with start, advance, stop methods + */ +async function progress(options) { + const clack = await getClack(); + return clack.progress(options); +} + +/** + * Create a task log for displaying scrolling subprocess output + * @param {Object} options - TaskLog options (title, limit, retainLog) + * @returns {Promise} TaskLog controller with message, success, error methods + */ +async function taskLog(options) { + const clack = await getClack(); + return clack.taskLog(options); +} + +/** + * File system path prompt with autocomplete + * @param {Object} options - Path options + * @param {string} options.message - The prompt message + * @param {string} [options.initialValue] - Initial path value + * @param {boolean} [options.directory=false] - Only allow directories + * @param {Function} [options.validate] - Validation function + * @returns {Promise} Selected path + */ +async function pathPrompt(options) { + const clack = await getClack(); + const result = await clack.path(options); + await handleCancel(result); + return result; +} + +/** + * Autocomplete single-select prompt with type-ahead filtering + * @param {Object} options - Autocomplete options + * @param {string} options.message - The prompt message + * @param {Array} options.options - Array of choices [{value, label, hint?}] + * @param {string} [options.placeholder] - Placeholder text + * @param {number} [options.maxItems] - Maximum visible items + * @param {Function} [options.filter] - Custom filter function + * @returns {Promise} Selected value + */ +async function autocomplete(options) { + const clack = await getClack(); + const result = await clack.autocomplete(options); + await handleCancel(result); + return result; +} + +/** + * Key-based instant selection prompt + * @param {Object} options - SelectKey options + * @param {string} options.message - The prompt message + * @param {Array} options.options - Array of choices [{value, label, hint?}] + * @returns {Promise} Selected value + */ +async function selectKey(options) { + const clack = await getClack(); + const result = await clack.selectKey(options); + await handleCancel(result); + return result; +} + +/** + * Stream messages with dynamic content (for LLMs, generators, etc.) + */ +const stream = { + async info(generator) { + const clack = await getClack(); + return clack.stream.info(generator); + }, + async success(generator) { + const clack = await getClack(); + return clack.stream.success(generator); + }, + async step(generator) { + const clack = await getClack(); + return clack.stream.step(generator); + }, + async warn(generator) { + const clack = await getClack(); + return clack.stream.warn(generator); + }, + async error(generator) { + const clack = await getClack(); + return clack.stream.error(generator); + }, + async message(generator, options) { + const clack = await getClack(); + return clack.stream.message(generator, options); + }, +}; + +/** + * Get the color utility (picocolors instance from @clack/prompts) + * @returns {Promise} The color utility (picocolors) + */ +async function getColor() { + return await getPicocolors(); +} + /** * Execute an array of Inquirer-style questions using @clack/prompts * This provides compatibility with dynamic question arrays @@ -415,19 +782,28 @@ async function prompt(questions) { module.exports = { getClack, + getColor, handleCancel, intro, outro, + cancel, note, + box, spinner, + progress, + taskLog, select, multiselect, - groupMultiselect, + autocompleteMultiselect, + autocomplete, + selectKey, confirm, text, + path: pathPrompt, password, group, tasks, log, + stream, prompt, }; diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index da5420cb2..9134b4e28 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -1,4 +1,3 @@ -const chalk = require('chalk'); const path = require('node:path'); const os = require('node:os'); const fs = require('fs-extra'); @@ -26,17 +25,31 @@ const choiceUtils = { Separator }; class UI { /** * Prompt for installation configuration + * @param {Object} options - Command-line options from install command * @returns {Object} Installation configuration */ - async promptInstall() { - CLIUtils.displayLogo(); + async promptInstall(options = {}) { + await CLIUtils.displayLogo(); // Display version-specific start message from install-messages.yaml const { MessageLoader } = require('../installers/lib/message-loader'); const messageLoader = new MessageLoader(); - messageLoader.displayStartMessage(); + await messageLoader.displayStartMessage(); - const confirmedDirectory = await this.getConfirmedDirectory(); + // Get directory from options or prompt + let confirmedDirectory; + if (options.directory) { + // Use provided directory from command-line + const expandedDir = this.expandUserPath(options.directory); + const validation = this.validateDirectorySync(expandedDir); + if (validation) { + throw new Error(`Invalid directory: ${validation}`); + } + confirmedDirectory = expandedDir; + await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`); + } else { + confirmedDirectory = await this.getConfirmedDirectory(); + } // Preflight: Check for legacy BMAD v4 footprints immediately after getting directory const { Detector } = require('../installers/lib/core/detector'); @@ -61,7 +74,7 @@ class UI { for (const entry of entries) { if (entry.isDirectory() && (entry.name === '.bmad' || entry.name === 'bmad')) { hasLegacyBmadFolder = true; - legacyBmadPath = path.join(confirmedDirectory, '.bmad'); + legacyBmadPath = path.join(confirmedDirectory, entry.name); bmadDir = legacyBmadPath; // Check if it has _cfg folder @@ -84,38 +97,30 @@ class UI { // Handle legacy .bmad or _cfg folder - these are very old (v4 or alpha) // Show version warning instead of offering conversion if (hasLegacyBmadFolder || hasLegacyCfg) { - console.log(''); - console.log(chalk.yellow.bold('⚠️ LEGACY INSTALLATION DETECTED')); - console.log(chalk.yellow('─'.repeat(80))); - console.log( - chalk.yellow( - 'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder - this is from a old BMAD version that is out of date for automatic upgrade, manual intervention required.', - ), + await prompts.log.warn('LEGACY INSTALLATION DETECTED'); + await prompts.note( + 'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder -\n' + + 'this is from an old BMAD version that is out of date for automatic upgrade,\n' + + 'manual intervention required.\n\n' + + 'You have a legacy version installed (v4 or alpha).\n' + + 'Legacy installations may have compatibility issues.\n\n' + + 'For the best experience, we strongly recommend:\n' + + ' 1. Delete your current BMAD installation folder (.bmad or bmad)\n' + + ' 2. Run a fresh installation\n\n' + + 'If you do not want to start fresh, you can attempt to proceed beyond this\n' + + 'point IF you have ensured the bmad folder is named _bmad, and under it there\n' + + 'is a _config folder. If you have a folder under your bmad folder named _cfg,\n' + + 'you would need to rename it _config, and then restart the installer.\n\n' + + 'Benefits of a fresh install:\n' + + ' \u2022 Cleaner configuration without legacy artifacts\n' + + ' \u2022 All new features properly configured\n' + + ' \u2022 Fewer potential conflicts\n\n' + + 'If you have already produced output from an earlier alpha version, you can\n' + + 'still retain those artifacts. After installation, ensure you configured during\n' + + 'install the proper file locations for artifacts depending on the module you\n' + + 'are using, or move the files to the proper locations.', + 'Legacy Installation Detected', ); - console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).')); - console.log(''); - console.log(chalk.dim('Legacy installations may have compatibility issues.')); - console.log(''); - console.log(chalk.dim('For the best experience, we strongly recommend:')); - console.log(chalk.dim(' 1. Delete your current BMAD installation folder (.bmad or bmad)')); - console.log( - chalk.dim( - ' 2. Run a fresh installation\n\nIf you do not want to start fresh, you can attempt to proceed beyond this point IF you have ensured the bmad folder is named _bmad, and under it there is a _config folder. If you have a folder under your bmad folder named _cfg, you would need to rename it _config, and then restart the installer.', - ), - ); - console.log(''); - console.log(chalk.dim('Benefits of a fresh install:')); - console.log(chalk.dim(' • Cleaner configuration without legacy artifacts')); - console.log(chalk.dim(' • All new features properly configured')); - console.log(chalk.dim(' • Fewer potential conflicts')); - console.log(chalk.dim('')); - console.log( - chalk.dim( - 'If you have already produced output from an earlier alpha version, you can still retain those artifacts. After installation, ensure you configured during install the proper file locations for artifacts depending on the module you are using, or move the files to the proper locations.', - ), - ); - console.log(chalk.yellow('─'.repeat(80))); - console.log(''); const proceed = await prompts.select({ message: 'How would you like to proceed?', @@ -133,37 +138,33 @@ class UI { }); if (proceed === 'cancel') { - console.log(''); - console.log(chalk.cyan('To do a fresh install:')); - console.log(chalk.dim(' 1. Delete the existing bmad folder in your project')); - console.log(chalk.dim(" 2. Run 'bmad install' again")); - console.log(''); + await prompts.note('1. Delete the existing bmad folder in your project\n' + "2. Run 'bmad install' again", 'To do a fresh install'); process.exit(0); return; } - const ora = require('ora'); - const spinner = ora('Updating folder structure...').start(); + const s = await prompts.spinner(); + s.start('Updating folder structure...'); try { // Handle .bmad folder if (hasLegacyBmadFolder) { const newBmadPath = path.join(confirmedDirectory, '_bmad'); await fs.move(legacyBmadPath, newBmadPath); bmadDir = newBmadPath; - spinner.succeed('Renamed ".bmad" to "_bmad"'); + s.stop(`Renamed "${path.basename(legacyBmadPath)}" to "_bmad"`); } // Handle _cfg folder (either from .bmad or standalone) const cfgPath = path.join(bmadDir, '_cfg'); if (await fs.pathExists(cfgPath)) { - spinner.start('Renaming configuration folder...'); + s.start('Renaming configuration folder...'); const newCfgPath = path.join(bmadDir, '_config'); await fs.move(cfgPath, newCfgPath); - spinner.succeed('Renamed "_cfg" to "_config"'); + s.stop('Renamed "_cfg" to "_config"'); } } catch (error) { - spinner.fail('Failed to update folder structure'); - console.error(chalk.red(`Error: ${error.message}`)); + s.stop('Failed to update folder structure'); + await prompts.log.error(`Error: ${error.message}`); process.exit(1); } } @@ -218,11 +219,21 @@ class UI { // Common actions choices.push({ name: 'Modify BMAD Installation', value: 'update' }); - actionType = await prompts.select({ - message: 'How would you like to proceed?', - choices: choices, - default: choices[0].value, - }); + // Check if action is provided via command-line + if (options.action) { + const validActions = choices.map((c) => c.value); + if (!validActions.includes(options.action)) { + throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`); + } + actionType = options.action; + await prompts.log.info(`Using action from command-line: ${actionType}`); + } else { + actionType = await prompts.select({ + message: 'How would you like to proceed?', + choices: choices, + default: choices[0].value, + }); + } // Handle quick update separately if (actionType === 'quick-update') { @@ -250,33 +261,97 @@ class UI { // Get existing installation info const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory); - console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`)); + await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`); // Unified module selection - all modules in one grouped multiselect - let selectedModules = await this.selectAllModules(installedModuleIds); + let selectedModules; + if (options.modules) { + // Use modules from command-line + selectedModules = options.modules + .split(',') + .map((m) => m.trim()) + .filter(Boolean); + await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); + } else { + selectedModules = await this.selectAllModules(installedModuleIds); + selectedModules = selectedModules.filter((m) => m !== 'core'); + } // After module selection, ask about custom modules - console.log(''); - const changeCustomModules = await prompts.confirm({ - message: 'Modify custom modules, agents, or workflows?', - default: false, - }); - let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } }; - if (changeCustomModules) { - customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules); - } else { - // Preserve existing custom modules if user doesn't want to modify them - const { Installer } = require('../installers/lib/core/installer'); - const installer = new Installer(); - const { bmadDir } = await installer.findBmadDir(confirmedDirectory); - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (await fs.pathExists(cacheDir)) { - const entries = await fs.readdir(cacheDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) { - customModuleResult.selectedCustomModules.push(entry.name); + if (options.customContent) { + // Use custom content from command-line + const paths = options.customContent + .split(',') + .map((p) => p.trim()) + .filter(Boolean); + await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`); + + // Build custom content config similar to promptCustomContentSource + const customPaths = []; + const selectedModuleIds = []; + + for (const customPath of paths) { + const expandedPath = this.expandUserPath(customPath); + const validation = this.validateCustomContentPathSync(expandedPath); + if (validation) { + await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`); + continue; + } + + // Read module metadata + let moduleMeta; + try { + const moduleYamlPath = path.join(expandedPath, 'module.yaml'); + const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8'); + const yaml = require('yaml'); + moduleMeta = yaml.parse(moduleYaml); + } catch (error) { + await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`); + continue; + } + + if (!moduleMeta.code) { + await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); + continue; + } + + customPaths.push(expandedPath); + selectedModuleIds.push(moduleMeta.code); + } + + if (customPaths.length > 0) { + customModuleResult = { + selectedCustomModules: selectedModuleIds, + customContentConfig: { + hasCustomContent: true, + paths: customPaths, + selectedModuleIds: selectedModuleIds, + }, + }; + } + } else { + const changeCustomModules = await prompts.confirm({ + message: 'Modify custom modules, agents, or workflows?', + default: false, + }); + + if (changeCustomModules) { + customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules); + } else { + // Preserve existing custom modules if user doesn't want to modify them + const { Installer } = require('../installers/lib/core/installer'); + const installer = new Installer(); + const { bmadDir } = await installer.findBmadDir(confirmedDirectory); + + const cacheDir = path.join(bmadDir, '_config', 'custom'); + if (await fs.pathExists(cacheDir)) { + const entries = await fs.readdir(cacheDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + customModuleResult.selectedCustomModules.push(entry.name); + } } } } @@ -288,9 +363,9 @@ class UI { } // Get tool selection - const toolSelection = await this.promptToolSelection(confirmedDirectory); + const toolSelection = await this.promptToolSelection(confirmedDirectory, options); - const coreConfig = await this.collectCoreConfig(confirmedDirectory); + const coreConfig = await this.collectCoreConfig(confirmedDirectory, options); return { actionType: 'update', @@ -309,16 +384,80 @@ class UI { const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory); // Unified module selection - all modules in one grouped multiselect - let selectedModules = await this.selectAllModules(installedModuleIds); + let selectedModules; + if (options.modules) { + // Use modules from command-line + selectedModules = options.modules + .split(',') + .map((m) => m.trim()) + .filter(Boolean); + await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); + } else if (options.yes) { + // Use default modules when --yes flag is set + selectedModules = await this.getDefaultModules(installedModuleIds); + await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`); + } else { + selectedModules = await this.selectAllModules(installedModuleIds); + } // Ask about custom content (local modules/agents/workflows) - const wantsCustomContent = await prompts.confirm({ - message: 'Add custom modules, agents, or workflows from your computer?', - default: false, - }); + if (options.customContent) { + // Use custom content from command-line + const paths = options.customContent + .split(',') + .map((p) => p.trim()) + .filter(Boolean); + await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`); - if (wantsCustomContent) { - customContentConfig = await this.promptCustomContentSource(); + // Build custom content config similar to promptCustomContentSource + const customPaths = []; + const selectedModuleIds = []; + + for (const customPath of paths) { + const expandedPath = this.expandUserPath(customPath); + const validation = this.validateCustomContentPathSync(expandedPath); + if (validation) { + await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`); + continue; + } + + // Read module metadata + let moduleMeta; + try { + const moduleYamlPath = path.join(expandedPath, 'module.yaml'); + const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8'); + const yaml = require('yaml'); + moduleMeta = yaml.parse(moduleYaml); + } catch (error) { + await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`); + continue; + } + + if (!moduleMeta.code) { + await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); + continue; + } + + customPaths.push(expandedPath); + selectedModuleIds.push(moduleMeta.code); + } + + if (customPaths.length > 0) { + customContentConfig = { + hasCustomContent: true, + paths: customPaths, + selectedModuleIds: selectedModuleIds, + }; + } + } else if (!options.yes) { + const wantsCustomContent = await prompts.confirm({ + message: 'Add custom modules, agents, or workflows from your computer?', + default: false, + }); + + if (wantsCustomContent) { + customContentConfig = await this.promptCustomContentSource(); + } } // Add custom content modules if any were selected @@ -327,8 +466,8 @@ class UI { } selectedModules = selectedModules.filter((m) => m !== 'core'); - let toolSelection = await this.promptToolSelection(confirmedDirectory); - const coreConfig = await this.collectCoreConfig(confirmedDirectory); + let toolSelection = await this.promptToolSelection(confirmedDirectory, options); + const coreConfig = await this.collectCoreConfig(confirmedDirectory, options); return { actionType: 'install', @@ -339,15 +478,20 @@ class UI { skipIde: toolSelection.skipIde, coreConfig: coreConfig, customContent: customContentConfig, + skipPrompts: options.yes || false, }; } /** * Prompt for tool/IDE selection (called after module configuration) + * Uses a split prompt approach: + * 1. Recommended tools - standard multiselect for 3 preferred tools + * 2. Additional tools - autocompleteMultiselect with search capability * @param {string} projectDir - Project directory to check for existing IDEs + * @param {Object} options - Command-line options * @returns {Object} Tool configuration */ - async promptToolSelection(projectDir) { + async promptToolSelection(projectDir, options = {}) { // Check for existing configured IDEs - use findBmadDir to detect custom folder names const { Detector } = require('../installers/lib/core/detector'); const { Installer } = require('../installers/lib/core/installer'); @@ -366,95 +510,151 @@ class UI { const preferredIdes = ideManager.getPreferredIdes(); const otherIdes = ideManager.getOtherIdes(); - // Build grouped options object for groupMultiselect - const groupedOptions = {}; - const processedIdes = new Set(); - const initialValues = []; + // Determine which configured IDEs are in "preferred" vs "other" categories + const configuredPreferred = configuredIdes.filter((id) => preferredIdes.some((ide) => ide.value === id)); + const configuredOther = configuredIdes.filter((id) => otherIdes.some((ide) => ide.value === id)); - // First, add previously configured IDEs, marked with ✅ + // Warn about previously configured tools that are no longer available + const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value)); + const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id)); + if (unknownTools.length > 0) { + await prompts.log.warn(`Previously configured tools are no longer available: ${unknownTools.join(', ')}`); + } + + // ───────────────────────────────────────────────────────────────────────────── + // UPGRADE PATH: If tools already configured, show all tools with configured at top + // ───────────────────────────────────────────────────────────────────────────── if (configuredIdes.length > 0) { - const configuredGroup = []; - for (const ideValue of configuredIdes) { - // Skip empty or invalid IDE values - if (!ideValue || typeof ideValue !== 'string') { - continue; - } + const allTools = [...preferredIdes, ...otherIdes]; - // Find the IDE in either preferred or other lists - const preferredIde = preferredIdes.find((ide) => ide.value === ideValue); - const otherIde = otherIdes.find((ide) => ide.value === ideValue); - const ide = preferredIde || otherIde; + // Sort: configured tools first, then preferred, then others + const sortedTools = [ + ...allTools.filter((ide) => configuredIdes.includes(ide.value)), + ...allTools.filter((ide) => !configuredIdes.includes(ide.value)), + ]; - if (ide) { - configuredGroup.push({ - label: `${ide.name} ✅`, - value: ide.value, - }); - processedIdes.add(ide.value); - initialValues.push(ide.value); // Pre-select configured IDEs - } else { - // Warn about unrecognized IDE (but don't fail) - console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`)); - } - } - if (configuredGroup.length > 0) { - groupedOptions['Previously Configured'] = configuredGroup; - } - } - - // Add preferred tools (excluding already processed) - const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value)); - if (remainingPreferred.length > 0) { - groupedOptions['Recommended Tools'] = remainingPreferred.map((ide) => { - processedIdes.add(ide.value); - return { - label: `${ide.name} ⭐`, - value: ide.value, - }; + const upgradeOptions = sortedTools.map((ide) => { + const isConfigured = configuredIdes.includes(ide.value); + const isPreferred = preferredIdes.some((p) => p.value === ide.value); + let label = ide.name; + if (isPreferred) label += ' ⭐'; + if (isConfigured) label += ' ✅'; + return { label, value: ide.value }; }); + + // Sort initialValues to match display order + const sortedInitialValues = sortedTools.filter((ide) => configuredIdes.includes(ide.value)).map((ide) => ide.value); + + const upgradeSelected = await prompts.autocompleteMultiselect({ + message: 'Integrate with', + options: upgradeOptions, + initialValues: sortedInitialValues, + required: false, + maxItems: 8, + }); + + const selectedIdes = upgradeSelected || []; + + if (selectedIdes.length === 0) { + const confirmNoTools = await prompts.confirm({ + message: 'No tools selected. Continue without installing any tools?', + default: false, + }); + + if (!confirmNoTools) { + return this.promptToolSelection(projectDir, options); + } + + return { ides: [], skipIde: true }; + } + + // Display selected tools + await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); + + return { ides: selectedIdes, skipIde: false }; } - // Add other tools (excluding already processed) - const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value)); - if (remainingOther.length > 0) { - groupedOptions['Additional Tools'] = remainingOther.map((ide) => ({ - label: ide.name, + // ───────────────────────────────────────────────────────────────────────────── + // NEW INSTALL: Show all tools with search + // ───────────────────────────────────────────────────────────────────────────── + const allTools = [...preferredIdes, ...otherIdes]; + + const allToolOptions = allTools.map((ide) => { + const isPreferred = preferredIdes.some((p) => p.value === ide.value); + let label = ide.name; + if (isPreferred) label += ' ⭐'; + return { + label, value: ide.value, - })); - } - - // Add standalone "None" option at the end - groupedOptions[' '] = [ - { - label: '⚠ None - I am not installing any tools', - value: '__NONE__', - }, - ]; + }; + }); let selectedIdes = []; - selectedIdes = await prompts.groupMultiselect({ - message: `Select tools to configure ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`, - options: groupedOptions, - initialValues: initialValues.length > 0 ? initialValues : undefined, - required: true, - selectableGroups: false, - }); - - // If user selected both "__NONE__" and other tools, honor the "None" choice - if (selectedIdes && selectedIdes.includes('__NONE__') && selectedIdes.length > 1) { - console.log(); - console.log(chalk.yellow('⚠️ "None - I am not installing any tools" was selected, so no tools will be configured.')); - console.log(); - selectedIdes = []; - } else if (selectedIdes && selectedIdes.includes('__NONE__')) { - // Only "__NONE__" was selected - selectedIdes = []; + // Check if tools are provided via command-line + if (options.tools) { + // Check for explicit "none" value to skip tool installation + if (options.tools.toLowerCase() === 'none') { + await prompts.log.info('Skipping tool configuration (--tools none)'); + return { ides: [], skipIde: true }; + } else { + selectedIdes = options.tools + .split(',') + .map((t) => t.trim()) + .filter(Boolean); + await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`); + await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); + return { ides: selectedIdes, skipIde: false }; + } + } else if (options.yes) { + // If --yes flag is set, skip tool prompt and use previously configured tools or empty + if (configuredIdes.length > 0) { + await prompts.log.info(`Using previously configured tools (--yes flag): ${configuredIdes.join(', ')}`); + await this.displaySelectedTools(configuredIdes, preferredIdes, allTools); + return { ides: configuredIdes, skipIde: false }; + } else { + await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)'); + return { ides: [], skipIde: true }; + } } + // Interactive mode + const interactiveSelectedIdes = await prompts.autocompleteMultiselect({ + message: 'Integrate with:', + options: allToolOptions, + initialValues: configuredIdes.length > 0 ? configuredIdes : undefined, + required: false, + maxItems: 8, + }); + + selectedIdes = interactiveSelectedIdes || []; + + // ───────────────────────────────────────────────────────────────────────────── + // STEP 3: Confirm if no tools selected + // ───────────────────────────────────────────────────────────────────────────── + if (selectedIdes.length === 0) { + const confirmNoTools = await prompts.confirm({ + message: 'No tools selected. Continue without installing any tools?', + default: false, + }); + + if (!confirmNoTools) { + // User wants to select tools - recurse + return this.promptToolSelection(projectDir, options); + } + + return { + ides: [], + skipIde: true, + }; + } + + // Display selected tools + await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); + return { - ides: selectedIdes || [], - skipIde: !selectedIdes || selectedIdes.length === 0, + ides: selectedIdes, + skipIde: selectedIdes.length === 0, }; } @@ -493,15 +693,12 @@ class UI { * Display installation summary * @param {Object} result - Installation result */ - showInstallSummary(result) { - // Clean, simple completion message - console.log('\n' + chalk.green.bold('✨ BMAD is ready to use!')); - - // Show installation summary in a simple format - console.log(chalk.dim(`Installed to: ${result.path}`)); + async showInstallSummary(result) { + let summary = `Installed to: ${result.path}`; if (result.modules && result.modules.length > 0) { - console.log(chalk.dim(`Modules: ${result.modules.join(', ')}`)); + summary += `\nModules: ${result.modules.join(', ')}`; } + await prompts.note(summary, 'BMAD is ready to use!'); } /** @@ -542,15 +739,75 @@ class UI { /** * Collect core configuration * @param {string} directory - Installation directory + * @param {Object} options - Command-line options * @returns {Object} Core configuration */ - async collectCoreConfig(directory) { + async collectCoreConfig(directory, options = {}) { const { ConfigCollector } = require('../installers/lib/core/config-collector'); const configCollector = new ConfigCollector(); - // Load existing configs first if they exist - await configCollector.loadExistingConfig(directory); - // Now collect with existing values as defaults (false = don't skip loading, true = skip completion message) - await configCollector.collectModuleConfig('core', directory, false, true); + + // If options are provided, set them directly + if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) { + const coreConfig = {}; + if (options.userName) { + coreConfig.user_name = options.userName; + await prompts.log.info(`Using user name from command-line: ${options.userName}`); + } + if (options.communicationLanguage) { + coreConfig.communication_language = options.communicationLanguage; + await prompts.log.info(`Using communication language from command-line: ${options.communicationLanguage}`); + } + if (options.documentOutputLanguage) { + coreConfig.document_output_language = options.documentOutputLanguage; + await prompts.log.info(`Using document output language from command-line: ${options.documentOutputLanguage}`); + } + if (options.outputFolder) { + coreConfig.output_folder = options.outputFolder; + await prompts.log.info(`Using output folder from command-line: ${options.outputFolder}`); + } + + // Load existing config to merge with provided options + await configCollector.loadExistingConfig(directory); + + // Merge provided options with existing config (or defaults) + const existingConfig = configCollector.collectedConfig.core || {}; + configCollector.collectedConfig.core = { ...existingConfig, ...coreConfig }; + + // If not all options are provided, collect the missing ones interactively (unless --yes flag) + if ( + !options.yes && + (!options.userName || !options.communicationLanguage || !options.documentOutputLanguage || !options.outputFolder) + ) { + await configCollector.collectModuleConfig('core', directory, false, true); + } + } else if (options.yes) { + // Use all defaults when --yes flag is set + await configCollector.loadExistingConfig(directory); + const existingConfig = configCollector.collectedConfig.core || {}; + + // If no existing config, use defaults + if (Object.keys(existingConfig).length === 0) { + let safeUsername; + try { + safeUsername = os.userInfo().username; + } catch { + safeUsername = process.env.USER || process.env.USERNAME || 'User'; + } + const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1); + configCollector.collectedConfig.core = { + user_name: defaultUsername, + communication_language: 'English', + document_output_language: 'English', + output_folder: '_bmad-output', + }; + await prompts.log.info('Using default configuration (--yes flag)'); + } + } else { + // Load existing configs first if they exist + await configCollector.loadExistingConfig(directory); + // Now collect with existing values as defaults (false = don't skip loading, true = skip completion message) + await configCollector.collectModuleConfig('core', directory, false, true); + } const coreConfig = configCollector.collectedConfig.core; // Ensure we always have a core config object, even if empty @@ -564,11 +821,11 @@ class UI { * @returns {Array} Module choices for prompt */ async getModuleChoices(installedModuleIds, customContentConfig = null) { + const color = await prompts.getColor(); const moduleChoices = []; const isNewInstallation = installedModuleIds.size === 0; const customContentItems = []; - const hasCustomContentItems = false; // Add custom content items if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) { @@ -580,7 +837,7 @@ class UI { const customInfo = await customHandler.getCustomInfo(customFile); if (customInfo) { customContentItems.push({ - name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`, + name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`, value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content checked: true, // Default to selected since user chose to provide custom content path: customInfo.path, // Track path to avoid duplicates @@ -608,7 +865,7 @@ class UI { if (!isDuplicate) { allCustomModules.push({ - name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(cached)`)}`, + name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`, value: mod.id, checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id), hint: mod.description || undefined, @@ -659,22 +916,20 @@ class UI { ...choicesWithDefaults, { value: '__NONE__', - label: '⚠ None / I changed my mind - skip module installation', + label: '\u26A0 None / I changed my mind - skip module installation', checked: false, }, ]; const selected = await prompts.multiselect({ - message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`, + message: 'Select modules to install (use arrow keys, space to toggle):', choices: choicesWithSkipOption, required: true, }); // If user selected both "__NONE__" and other items, honor the "None" choice if (selected && selected.includes('__NONE__') && selected.length > 1) { - console.log(); - console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no modules will be installed.')); - console.log(); + await prompts.log.warn('"None / I changed my mind" was selected, so no modules will be installed.'); return []; } @@ -707,8 +962,7 @@ class UI { */ async selectExternalModules(externalModuleChoices, defaultSelections = []) { // Build a message showing available modules - const availableNames = externalModuleChoices.map((c) => c.name).join(', '); - const message = `Select official BMad modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`; + const message = 'Select official BMad modules to install (use arrow keys, space to toggle):'; // Mark choices as checked based on defaultSelections const choicesWithDefaults = externalModuleChoices.map((choice) => ({ @@ -734,9 +988,7 @@ class UI { // If user selected both "__NONE__" and other items, honor the "None" choice if (selected && selected.includes('__NONE__') && selected.length > 1) { - console.log(); - console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no external modules will be installed.')); - console.log(); + await prompts.log.warn('"None / I changed my mind" was selected, so no external modules will be installed.'); return []; } @@ -758,100 +1010,125 @@ class UI { const externalManager = new ExternalModuleManager(); const externalModules = await externalManager.listAvailable(); - // Build grouped options - const groupedOptions = {}; + // Build flat options list with group hints for autocompleteMultiselect + const allOptions = []; const initialValues = []; + const lockedValues = ['core']; + + // Core module is always installed — show it locked at the top + allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' }); + initialValues.push('core'); // Helper to build module entry with proper sorting and selection - const buildModuleEntry = (mod, value) => { + const buildModuleEntry = (mod, value, group) => { const isInstalled = installedModuleIds.has(value); - const isDefault = mod.defaultSelected === true; return { - label: mod.description ? `${mod.name} — ${mod.description}` : mod.name, + label: mod.name, value, - // For sorting: defaultSelected=0, others=1 - sortKey: isDefault ? 0 : 1, - // Pre-select if default selected OR already installed - selected: isDefault || isInstalled, + hint: mod.description || group, + // Pre-select only if already installed (not on fresh install) + selected: isInstalled, }; }; - // Group 1: BMad Core (BMM, BMB) - const coreModules = []; + // Local modules (BMM, BMB, etc.) + const localEntries = []; for (const mod of localModules) { - if (!mod.isCustom && (mod.id === 'bmm' || mod.id === 'bmb')) { - const entry = buildModuleEntry(mod, mod.id); - coreModules.push(entry); + if (!mod.isCustom && mod.id !== 'core') { + const entry = buildModuleEntry(mod, mod.id, 'Local'); + localEntries.push(entry); if (entry.selected) { initialValues.push(mod.id); } } } - // Sort: defaultSelected first, then others - coreModules.sort((a, b) => a.sortKey - b.sortKey); - // Remove sortKey from final entries - if (coreModules.length > 0) { - groupedOptions['BMad Core'] = coreModules.map(({ label, value }) => ({ label, value })); - } + allOptions.push(...localEntries.map(({ label, value, hint }) => ({ label, value, hint }))); // Group 2: BMad Official Modules (type: bmad-org) const officialModules = []; for (const mod of externalModules) { if (mod.type === 'bmad-org') { - const entry = buildModuleEntry(mod, mod.code); + const entry = buildModuleEntry(mod, mod.code, 'Official'); officialModules.push(entry); if (entry.selected) { initialValues.push(mod.code); } } } - officialModules.sort((a, b) => a.sortKey - b.sortKey); - if (officialModules.length > 0) { - groupedOptions['BMad Official Modules'] = officialModules.map(({ label, value }) => ({ label, value })); - } + allOptions.push(...officialModules.map(({ label, value, hint }) => ({ label, value, hint }))); // Group 3: Community Modules (type: community) const communityModules = []; for (const mod of externalModules) { if (mod.type === 'community') { - const entry = buildModuleEntry(mod, mod.code); + const entry = buildModuleEntry(mod, mod.code, 'Community'); communityModules.push(entry); if (entry.selected) { initialValues.push(mod.code); } } } - communityModules.sort((a, b) => a.sortKey - b.sortKey); - if (communityModules.length > 0) { - groupedOptions['Community Modules'] = communityModules.map(({ label, value }) => ({ label, value })); - } + allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })), { + // "None" option at the end + label: '\u26A0 None - Skip module installation', + value: '__NONE__', + }); - // Add "None" option at the end - groupedOptions[' '] = [ - { - label: '⚠ None - Skip module installation', - value: '__NONE__', - }, - ]; - - const selected = await prompts.groupMultiselect({ - message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`, - options: groupedOptions, + const selected = await prompts.autocompleteMultiselect({ + message: 'Select modules to install:', + options: allOptions, initialValues: initialValues.length > 0 ? initialValues : undefined, + lockedValues, required: true, - selectableGroups: false, + maxItems: allOptions.length, }); // If user selected both "__NONE__" and other items, honor the "None" choice if (selected && selected.includes('__NONE__') && selected.length > 1) { - console.log(); - console.log(chalk.yellow('⚠️ "None" was selected, so no modules will be installed.')); - console.log(); + await prompts.log.warn('"None" was selected, so no modules will be installed.'); return []; } // Filter out the special '__NONE__' value - return selected ? selected.filter((m) => m !== '__NONE__') : []; + const result = selected ? selected.filter((m) => m !== '__NONE__') : []; + + // Display selected modules as bulleted list + if (result.length > 0) { + const moduleLines = result.map((moduleId) => { + const opt = allOptions.find((o) => o.value === moduleId); + return ` \u2022 ${opt?.label || moduleId}`; + }); + await prompts.log.message('Selected modules:\n' + moduleLines.join('\n')); + } + + return result; + } + + /** + * Get default modules for non-interactive mode + * @param {Set} installedModuleIds - Already installed module IDs + * @returns {Array} Default module codes + */ + async getDefaultModules(installedModuleIds = new Set()) { + const { ModuleManager } = require('../installers/lib/modules/manager'); + const moduleManager = new ModuleManager(); + const { modules: localModules } = await moduleManager.listAvailable(); + + const defaultModules = []; + + // Add default-selected local modules (typically BMM) + for (const mod of localModules) { + if (mod.defaultSelected === true || installedModuleIds.has(mod.id)) { + defaultModules.push(mod.id); + } + } + + // If no defaults found, use 'bmm' as the fallback default + if (defaultModules.length === 0) { + defaultModules.push('bmm'); + } + + return defaultModules; } /** @@ -883,7 +1160,7 @@ class UI { * @param {string} directory - The directory path */ async displayDirectoryInfo(directory) { - console.log(chalk.cyan('\nResolved installation path:'), chalk.bold(directory)); + await prompts.log.info(`Resolved installation path: ${directory}`); const dirExists = await fs.pathExists(directory); if (dirExists) { @@ -899,12 +1176,10 @@ class UI { const hasBmadInstall = (await fs.pathExists(bmadResult.bmadDir)) && (await fs.pathExists(path.join(bmadResult.bmadDir, '_config', 'manifest.yaml'))); - console.log( - chalk.gray(`Directory exists and contains ${files.length} item(s)`) + - (hasBmadInstall ? chalk.yellow(` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})`) : ''), - ); + const bmadNote = hasBmadInstall ? ` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})` : ''; + await prompts.log.message(`Directory exists and contains ${files.length} item(s)${bmadNote}`); } else { - console.log(chalk.gray('Directory exists and is empty')); + await prompts.log.message('Directory exists and is empty'); } } } @@ -925,7 +1200,7 @@ class UI { }); if (!proceed) { - console.log(chalk.yellow("\nLet's try again with a different path.\n")); + await prompts.log.warn("Let's try again with a different path."); } return proceed; @@ -937,7 +1212,7 @@ class UI { }); if (!create) { - console.log(chalk.yellow("\nLet's try again with a different path.\n")); + await prompts.log.warn("Let's try again with a different path."); } return create; @@ -1157,7 +1432,7 @@ class UI { return configs; } catch { // If loading fails, return empty configs - console.warn('Warning: Could not load existing configurations'); + await prompts.log.warn('Could not load existing configurations'); return configs; } } @@ -1288,7 +1563,7 @@ class UI { name: moduleData.name || moduleData.code, }); - console.log(chalk.green(`✓ Confirmed local custom module: ${moduleData.name || moduleData.code}`)); + await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`); } // Ask if user wants to add these to the installation @@ -1354,11 +1629,11 @@ class UI { }; // Ask user about custom modules - console.log(chalk.cyan('\n⚙️ Custom Modules')); + await prompts.log.info('Custom Modules'); if (cachedCustomModules.length > 0) { - console.log(chalk.dim('Found custom modules in your installation:')); + await prompts.log.message('Found custom modules in your installation:'); } else { - console.log(chalk.dim('No custom modules currently installed.')); + await prompts.log.message('No custom modules currently installed.'); } // Build choices dynamically based on whether we have existing modules @@ -1384,14 +1659,14 @@ class UI { case 'keep': { // Keep all existing custom modules result.selectedCustomModules = cachedCustomModules.map((m) => m.id); - console.log(chalk.dim(`Keeping ${result.selectedCustomModules.length} custom module(s)`)); + await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`); break; } case 'select': { // Let user choose which to keep const selectChoices = cachedCustomModules.map((m) => ({ - name: `${m.name} ${chalk.gray(`(${m.id})`)}`, + name: `${m.name} (${m.id})`, value: m.id, checked: m.checked, })); @@ -1407,16 +1682,14 @@ class UI { ]; const keepModules = await prompts.multiselect({ - message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`, + message: 'Select custom modules to keep (use arrow keys, space to toggle):', choices: choicesWithSkip, required: true, }); // If user selected both "__NONE__" and other modules, honor the "None" choice if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) { - console.log(); - console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no custom modules will be kept.')); - console.log(); + await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.'); result.selectedCustomModules = []; } else { // Filter out the special '__NONE__' value @@ -1441,13 +1714,13 @@ class UI { case 'remove': { // Remove all custom modules - console.log(chalk.yellow('All custom modules will be removed from the installation')); + await prompts.log.warn('All custom modules will be removed from the installation'); break; } case 'cancel': { // User cancelled - no custom modules - console.log(chalk.dim('No custom modules will be added')); + await prompts.log.message('No custom modules will be added'); break; } } @@ -1480,30 +1753,26 @@ class UI { return true; // Not legacy, proceed } - console.log(''); - console.log(chalk.yellow.bold('⚠️ VERSION WARNING')); - console.log(chalk.yellow('─'.repeat(80))); - + let warningContent; if (installedVersion === 'unknown') { - console.log(chalk.yellow('Unable to detect your installed BMAD version.')); - console.log(chalk.yellow('This appears to be a legacy or unsupported installation.')); + warningContent = 'Unable to detect your installed BMAD version.\n' + 'This appears to be a legacy or unsupported installation.'; } else { - console.log(chalk.yellow(`You are updating from ${installedVersion} to ${currentVersion}.`)); - console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).')); + warningContent = + `You are updating from ${installedVersion} to ${currentVersion}.\n` + 'You have a legacy version installed (v4 or alpha).'; } - console.log(''); - console.log(chalk.dim('For the best experience, we recommend:')); - console.log(chalk.dim(' 1. Delete your current BMAD installation folder')); - console.log(chalk.dim(` (the "${bmadFolderName}/" folder in your project)`)); - console.log(chalk.dim(' 2. Run a fresh installation')); - console.log(''); - console.log(chalk.dim('Benefits of a fresh install:')); - console.log(chalk.dim(' • Cleaner configuration without legacy artifacts')); - console.log(chalk.dim(' • All new features properly configured')); - console.log(chalk.dim(' • Fewer potential conflicts')); - console.log(chalk.yellow('─'.repeat(80))); - console.log(''); + warningContent += + '\n\nFor the best experience, we recommend:\n' + + ' 1. Delete your current BMAD installation folder\n' + + ` (the "${bmadFolderName}/" folder in your project)\n` + + ' 2. Run a fresh installation\n\n' + + 'Benefits of a fresh install:\n' + + ' \u2022 Cleaner configuration without legacy artifacts\n' + + ' \u2022 All new features properly configured\n' + + ' \u2022 Fewer potential conflicts'; + + await prompts.log.warn('VERSION WARNING'); + await prompts.note(warningContent, 'Version Warning'); const proceed = await prompts.select({ message: 'How would you like to proceed?', @@ -1521,11 +1790,10 @@ class UI { }); if (proceed === 'cancel') { - console.log(''); - console.log(chalk.cyan('To do a fresh install:')); - console.log(chalk.dim(` 1. Delete the "${bmadFolderName}/" folder in your project`)); - console.log(chalk.dim(" 2. Run 'bmad install' again")); - console.log(''); + await prompts.note( + `1. Delete the "${bmadFolderName}/" folder in your project\n` + "2. Run 'bmad install' again", + 'To do a fresh install', + ); } return proceed === 'proceed'; @@ -1536,41 +1804,34 @@ class UI { * @param {Array} modules - Array of module info objects with version info * @param {Array} availableUpdates - Array of available updates */ - displayModuleVersions(modules, availableUpdates = []) { - console.log(''); - console.log(chalk.cyan.bold('📦 Module Versions')); - console.log(chalk.gray('─'.repeat(80))); - + async displayModuleVersions(modules, availableUpdates = []) { // Group modules by source const builtIn = modules.filter((m) => m.source === 'built-in'); const external = modules.filter((m) => m.source === 'external'); const custom = modules.filter((m) => m.source === 'custom'); const unknown = modules.filter((m) => m.source === 'unknown'); - const displayGroup = (group, title) => { + const lines = []; + const formatGroup = (group, title) => { if (group.length === 0) return; - - console.log(chalk.yellow(`\n${title}`)); - for (const module of group) { - const updateInfo = availableUpdates.find((u) => u.name === module.name); - const versionDisplay = module.version || chalk.gray('unknown'); - + lines.push(title); + for (const mod of group) { + const updateInfo = availableUpdates.find((u) => u.name === mod.name); + const versionDisplay = mod.version || 'unknown'; if (updateInfo) { - console.log( - ` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay} → ${chalk.green(updateInfo.latestVersion)} ${chalk.green('↑')}`, - ); + lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2192 ${updateInfo.latestVersion} \u2191`); } else { - console.log(` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay} ${chalk.gray('✓')}`); + lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2713`); } } }; - displayGroup(builtIn, 'Built-in Modules'); - displayGroup(external, 'External Modules (Official)'); - displayGroup(custom, 'Custom Modules'); - displayGroup(unknown, 'Other Modules'); + formatGroup(builtIn, 'Built-in Modules'); + formatGroup(external, 'External Modules (Official)'); + formatGroup(custom, 'Custom Modules'); + formatGroup(unknown, 'Other Modules'); - console.log(''); + await prompts.note(lines.join('\n'), 'Module Versions'); } /** @@ -1583,12 +1844,10 @@ class UI { return []; } - console.log(''); - console.log(chalk.cyan.bold('🔄 Available Updates')); - console.log(chalk.gray('─'.repeat(80))); + await prompts.log.info('Available Updates'); const choices = availableUpdates.map((update) => ({ - name: `${update.name} ${chalk.dim(`(v${update.installedVersion} → v${update.latestVersion})`)}`, + name: `${update.name} (v${update.installedVersion} \u2192 v${update.latestVersion})`, value: update.name, checked: true, // Default to selecting all updates })); @@ -1614,7 +1873,7 @@ class UI { // Allow specific selection const selected = await prompts.multiselect({ - message: `Select modules to update ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`, + message: 'Select modules to update (use arrow keys, space to toggle):', choices: choices, required: true, }); @@ -1626,34 +1885,48 @@ class UI { * Display status of all installed modules * @param {Object} statusData - Status data with modules, installation info, and available updates */ - displayStatus(statusData) { + async displayStatus(statusData) { const { installation, modules, availableUpdates, bmadDir } = statusData; - console.log(''); - console.log(chalk.cyan.bold('📋 BMAD Status')); - console.log(chalk.gray('─'.repeat(80))); - // Installation info - console.log(chalk.yellow('\nInstallation')); - console.log(` ${chalk.gray('Version:'.padEnd(20))} ${installation.version || chalk.gray('unknown')}`); - console.log(` ${chalk.gray('Location:'.padEnd(20))} ${bmadDir}`); - console.log(` ${chalk.gray('Installed:'.padEnd(20))} ${new Date(installation.installDate).toLocaleDateString()}`); - console.log( - ` ${chalk.gray('Last Updated:'.padEnd(20))} ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : chalk.gray('unknown')}`, - ); + const infoLines = [ + `Version: ${installation.version || 'unknown'}`, + `Location: ${bmadDir}`, + `Installed: ${new Date(installation.installDate).toLocaleDateString()}`, + `Last Updated: ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : 'unknown'}`, + ]; + + await prompts.note(infoLines.join('\n'), 'BMAD Status'); // Module versions - this.displayModuleVersions(modules, availableUpdates); + await this.displayModuleVersions(modules, availableUpdates); // Update summary if (availableUpdates.length > 0) { - console.log(chalk.yellow.bold(`\n⚠️ ${availableUpdates.length} update(s) available`)); - console.log(chalk.dim(` Run 'bmad install' and select "Quick Update" to update`)); + await prompts.log.warn(`${availableUpdates.length} update(s) available`); + await prompts.log.message('Run \'bmad install\' and select "Quick Update" to update'); } else { - console.log(chalk.green.bold('\n✓ All modules are up to date')); + await prompts.log.success('All modules are up to date'); } + } - console.log(''); + /** + * Display list of selected tools after IDE selection + * @param {Array} selectedIdes - Array of selected IDE values + * @param {Array} preferredIdes - Array of preferred IDE objects + * @param {Array} allTools - Array of all tool objects + */ + async displaySelectedTools(selectedIdes, preferredIdes, allTools) { + if (selectedIdes.length === 0) return; + + const preferredValues = new Set(preferredIdes.map((ide) => ide.value)); + const toolLines = selectedIdes.map((ideValue) => { + const tool = allTools.find((t) => t.value === ideValue); + const name = tool?.name || ideValue; + const marker = preferredValues.has(ideValue) ? ' \u2B50' : ''; + return ` \u2022 ${name}${marker}`; + }); + await prompts.log.message('Selected tools:\n' + toolLines.join('\n')); } } diff --git a/tools/docs/BUNDLE_DISTRIBUTION_SETUP.md b/tools/docs/BUNDLE_DISTRIBUTION_SETUP.md deleted file mode 100644 index f206ee7f2..000000000 --- a/tools/docs/BUNDLE_DISTRIBUTION_SETUP.md +++ /dev/null @@ -1,95 +0,0 @@ -# Bundle Distribution Setup (For Maintainers) - -**Audience:** BMAD maintainers setting up bundle auto-publishing - ---- - -## One-Time Setup - -Run these commands once to enable auto-publishing: - -```bash -# 1. Create bmad-bundles repo -gh repo create bmad-code-org/bmad-bundles --public --description "BMAD Web Bundles" - -# 2. Ensure `main` exists (GitHub Pages API requires a source branch) -git clone git@github.com:bmad-code-org/bmad-bundles.git -cd bmad-bundles -printf '# bmad-bundles\n\nStatic bundles published from BMAD-METHOD.\n' > README.md -git add README.md -git commit -m "Initial commit" -git push origin main -cd - - -# 3. Enable GitHub Pages (API replacement for removed --enable-pages flag) -gh api repos/bmad-code-org/bmad-bundles/pages --method POST -f source[branch]=main -f source[path]=/ -# (Optional) confirm status -gh api repos/bmad-code-org/bmad-bundles/pages --jq '{status,source}' - -# 4. Create GitHub PAT and add as secret -# Go to: https://github.com/settings/tokens/new -# Scopes: repo (full control) -# Name: bmad-bundles-ci -# Then add as secret: -gh secret set BUNDLES_PAT --repo bmad-code-org/BMAD-METHOD -# (paste PAT when prompted) -``` - -If the Pages POST returns `409`, the site already exists. If it returns `422` about `main` missing, redo step 2 to push the initial commit. - -**Done.** Bundles auto-publish on every main merge. - ---- - -## How It Works - -**On main merge:** - -- `.github/workflows/bundle-latest.yaml` runs -- Publishes to: `https://bmad-code-org.github.io/bmad-bundles/` - -**On release:** - -- `npm run release:patch` runs `.github/workflows/manual-release.yaml` -- Attaches bundles to: `https://github.com/bmad-code-org/BMAD-METHOD/releases/latest` - ---- - -## Testing - -```bash -# Test latest channel -git push origin main -# Wait 2 min, then: curl https://bmad-code-org.github.io/bmad-bundles/ - -# Test stable channel -npm run release:patch -# Check: gh release view -``` - ---- - -## Troubleshooting - -**"Permission denied" or auth errors** - -```bash -# Verify PAT secret exists -gh secret list --repo bmad-code-org/BMAD-METHOD | grep BUNDLES_PAT - -# If missing, recreate PAT and add secret: -gh secret set BUNDLES_PAT --repo bmad-code-org/BMAD-METHOD -``` - -**GitHub Pages not updating / need to re-check config** - -```bash -gh api repos/bmad-code-org/bmad-bundles/pages --jq '{status,source,html_url}' -``` - ---- - -## Distribution URLs - -**Stable:** `https://github.com/bmad-code-org/BMAD-METHOD/releases/latest` -**Latest:** `https://bmad-code-org.github.io/bmad-bundles/` diff --git a/tools/docs/_prompt-external-modules-page.md b/tools/docs/_prompt-external-modules-page.md new file mode 100644 index 000000000..f5e124373 --- /dev/null +++ b/tools/docs/_prompt-external-modules-page.md @@ -0,0 +1,59 @@ +# Prompt: Generate External Modules Reference Page + +## Goal + +Create a reference documentation page at `docs/reference/modules.md` that lists all official external BMad modules with descriptions and links. + +## Source of Truth + +Read `tools/cli/external-official-modules.yaml` — this is the authoritative registry of official external modules. Use the module names, codes, npm package names, and repository URLs from this file. + +## Research Step + +For each module in the registry, visit its GitHub repository (url in the YAML record) +and read its README to get: +- A 1-2 sentence description of what the module does +- The key agents and workflows it provides (if listed) +- Any notable features or use cases + +## Output Format + +Create `docs/reference/modules.md` following the project's Reference Catalog structure (see `docs/_STYLE_GUIDE.md`): + +``` +1. Title + Hook +2. Items (## for each module) + - Brief description (one sentence) + - **Key Info:** as flat list (code, npm package, GitHub link) +3. Installation note +``` + +## Style +use @docs/_STYLE_GUIDE.md + +## Frontmatter + +```yaml +--- +title: Official Modules +--- +``` + +## Content Requirements + +- Start with a brief intro explaining that BMad extends through official modules selected during installation +- For each module include: + - `##` header with module name + - 1-2 sentence description (sourced from GitHub README, not just the registry's short description) + - Key info list: module code, npm package (linked), GitHub repo (linked) + - Brief bullet list of what it provides (agents, workflows, key features) — keep to 3-5 bullets +- Include a `:::tip` admonition about how to install modules (via `npx bmad-method` installer) +- Mention that community modules and a marketplace are coming +- Do NOT include built-in modules (core, bmm) — this page is specifically for external/add-on modules + +## Existing Pages for Reference + +Look at these files to match the tone and style of existing reference docs: +- `docs/reference/agents.md` +- `docs/reference/commands.md` +- `docs/reference/testing.md` diff --git a/tools/docs/index.md b/tools/docs/index.md deleted file mode 100644 index 8ac7bc86f..000000000 --- a/tools/docs/index.md +++ /dev/null @@ -1,2 +0,0 @@ -# Tool and Repo Maintainability Documentation - diff --git a/tools/flattener/aggregate.js b/tools/flattener/aggregate.js deleted file mode 100644 index 6a597a2fe..000000000 --- a/tools/flattener/aggregate.js +++ /dev/null @@ -1,76 +0,0 @@ -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. - * Returns text files, binary files (with size), and errors. - * @param {string[]} files absolute file paths - * @param {string} rootDir - * @param {{ text?: string, warn?: (msg: string) => void } | null} spinner - */ -async function aggregateFileContents(files, rootDir, spinner = null) { - const results = { - textFiles: [], - binaryFiles: [], - errors: [], - totalFiles: files.length, - processedFiles: 0, - }; - - // 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; - 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))); - } - - async function processOne(filePath) { - try { - const relativePath = path.relative(rootDir, filePath); - if (spinner) { - spinner.text = `Processing: ${relativePath} (${results.processedFiles + 1}/${results.totalFiles})`; - } - - const binary = await isBinaryFile(filePath); - if (binary) { - const { size } = await fs.stat(filePath); - results.binaryFiles.push({ path: relativePath, absolutePath: filePath, size }); - } else { - const content = await fs.readFile(filePath, 'utf8'); - results.textFiles.push({ - path: relativePath, - absolutePath: filePath, - content, - size: content.length, - lines: content.split('\n').length, - }); - } - } catch (error) { - const relativePath = path.relative(rootDir, filePath); - const errorInfo = { path: relativePath, absolutePath: filePath, error: error.message }; - results.errors.push(errorInfo); - if (spinner) { - spinner.warn(`Warning: Could not read file ${relativePath}: ${error.message}`); - } else { - console.warn(`Warning: Could not read file ${relativePath}: ${error.message}`); - } - } finally { - results.processedFiles++; - } - } - - for (let index = 0; index < files.length; index += concurrency) { - const slice = files.slice(index, index + concurrency); - await Promise.all(slice.map(processOne)); - } - - return results; -} - -module.exports = { - aggregateFileContents, -}; diff --git a/tools/flattener/binary.js b/tools/flattener/binary.js deleted file mode 100644 index fcfb27c1a..000000000 --- a/tools/flattener/binary.js +++ /dev/null @@ -1,80 +0,0 @@ -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. - * - Fast path by extension for common binaries - * - Otherwise read a small prefix and check for NUL bytes - * @param {string} filePath - * @returns {Promise} - */ -async function isBinaryFile(filePath) { - try { - const stats = await fsp.stat(filePath); - if (stats.isDirectory()) { - 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', - ]); - - const extension = path.extname(filePath).toLowerCase(); - if (binaryExtensions.has(extension)) return true; - if (stats.size === 0) return false; - - const sampleSize = Math.min(4096, stats.size); - const fd = await fsp.open(filePath, 'r'); - try { - const buffer = Buffer.allocUnsafe(sampleSize); - const { bytesRead } = await fd.read(buffer, 0, sampleSize, 0); - const slice = bytesRead === sampleSize ? buffer : buffer.subarray(0, bytesRead); - return slice.includes(0); - } finally { - await fd.close(); - } - } catch (error) { - console.warn(`Warning: Could not determine if file is binary: ${filePath} - ${error.message}`); - return false; - } -} - -module.exports = { - isBinaryFile, -}; diff --git a/tools/flattener/discovery.js b/tools/flattener/discovery.js deleted file mode 100644 index 7eaaa2d40..000000000 --- a/tools/flattener/discovery.js +++ /dev/null @@ -1,71 +0,0 @@ -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' - ); - } catch { - return false; - } -} - -async function gitListFiles(rootDir) { - try { - const { stdout } = await pExecFile('git', ['ls-files', '-co', '--exclude-standard'], { - cwd: rootDir, - }); - return String(stdout || '') - .split(/\r?\n/) - .map((s) => s.trim()) - .filter(Boolean); - } catch { - return []; - } -} - -/** - * Discover files under rootDir. - * - Prefer git ls-files when available for speed/correctness - * - Fallback to glob and apply unified ignore rules - * @param {string} rootDir - * @param {object} [options] - * @param {boolean} [options.preferGit=true] - * @returns {Promise} absolute file paths - */ -async function discoverFiles(rootDir, options = {}) { - const { preferGit = true } = options; - const { filter } = await loadIgnore(rootDir); - - // Try git first - 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('**/*', { - cwd: rootDir, - nodir: true, - dot: true, - follow: false, - }); - const filteredRel = globbed.filter((p) => filter(p)); - return filteredRel.map((p) => path.resolve(rootDir, p)); -} - -module.exports = { - discoverFiles, -}; diff --git a/tools/flattener/files.js b/tools/flattener/files.js deleted file mode 100644 index e7236d7b0..000000000 --- a/tools/flattener/files.js +++ /dev/null @@ -1,35 +0,0 @@ -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) { - return await ignoreRules.parseGitignore(gitignorePath); -} - -async function discoverFiles(rootDir) { - try { - // 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); - return []; - } -} - -async function filterFiles(files, rootDir) { - const { filter } = await ignoreRules.loadIgnore(rootDir); - const relativeFiles = files.map((f) => path.relative(rootDir, f)); - const filteredRelative = relativeFiles.filter((p) => filter(p)); - return filteredRelative.map((p) => path.resolve(rootDir, p)); -} - -module.exports = { - parseGitignore, - discoverFiles, - isBinaryFile, - aggregateFileContents, - filterFiles, -}; diff --git a/tools/flattener/ignoreRules.js b/tools/flattener/ignoreRules.js deleted file mode 100644 index b825edea7..000000000 --- a/tools/flattener/ignoreRules.js +++ /dev/null @@ -1,172 +0,0 @@ -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/**', - '**/.git/**', - '**/.svn/**', - '**/.hg/**', - '**/.bzr/**', - // Package/build outputs - '**/node_modules/**', - '**/bower_components/**', - '**/vendor/**', - '**/packages/**', - '**/build/**', - '**/dist/**', - '**/out/**', - '**/target/**', - '**/bin/**', - '**/obj/**', - '**/release/**', - '**/debug/**', - // Environments - '**/.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/**', - // Caches & temp - '**/.cache/**', - '**/.tmp/**', - '**/.temp/**', - '**/tmp/**', - '**/temp/**', - '**/.sass-cache/**', - // IDE/editor - '**/.vscode/**', - '**/.idea/**', - '**/*.swp', - '**/*.swo', - '**/*~', - '**/.project', - '**/.classpath', - '**/.settings/**', - '**/*.sublime-project', - '**/*.sublime-workspace', - // Lockfiles - '**/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', - // 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', - // XML outputs - '**/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', - // Env files - '**/.env', - '**/.env.*', - '**/*.env', - // Misc - '**/junit.xml', -]; - -async function readIgnoreFile(filePath) { - try { - if (!(await fs.pathExists(filePath))) return []; - const content = await fs.readFile(filePath, 'utf8'); - return content - .split('\n') - .map((l) => l.trim()) - .filter((l) => l && !l.startsWith('#')); - } catch { - return []; - } -} - -// Backward compatible export matching previous signature -async function parseGitignore(gitignorePath) { - return readIgnoreFile(gitignorePath); -} - -async function loadIgnore(rootDir, extraPatterns = []) { - const ig = ignore(); - const gitignorePath = path.join(rootDir, '.gitignore'); - const patterns = [...(await readIgnoreFile(gitignorePath)), ...DEFAULT_PATTERNS, ...extraPatterns]; - // De-duplicate - const unique = [...new Set(patterns.map(String))]; - ig.add(unique); - - // Include-only filter: return true if path should be included - const filter = (relativePath) => !ig.ignores(relativePath.replaceAll('\\', '/')); - - return { ig, filter, patterns: unique }; -} - -module.exports = { - DEFAULT_PATTERNS, - parseGitignore, - loadIgnore, -}; diff --git a/tools/flattener/main.js b/tools/flattener/main.js deleted file mode 100644 index 72bb42f90..000000000 --- a/tools/flattener/main.js +++ /dev/null @@ -1,483 +0,0 @@ -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'); - -/** - * Recursively discover all files in a directory - * @param {string} rootDir - The root directory to scan - * @returns {Promise} Array of file paths - */ - -/** - * Parse .gitignore file and return ignore patterns - * @param {string} gitignorePath - Path to .gitignore file - * @returns {Promise} Array of ignore patterns - */ - -/** - * Check if a file is binary using file command and heuristics - * @param {string} filePath - Path to the file - * @returns {Promise} True if file is binary - */ - -/** - * Read and aggregate content from text files - * @param {string[]} files - Array of file paths - * @param {string} rootDir - The root directory - * @param {Object} spinner - Optional spinner instance for progress display - * @returns {Promise} Object containing file contents and metadata - */ - -/** - * Generate XML output with aggregated file contents using streaming - * @param {Object} aggregatedContent - The aggregated content object - * @param {string} outputPath - The output file path - * @returns {Promise} Promise that resolves when writing is complete - */ - -/** - * Calculate statistics for the processed files - * @param {Object} aggregatedContent - The aggregated content object - * @param {number} xmlFileSize - The size of the generated XML file in bytes - * @returns {Object} Statistics object - */ - -/** - * Filter files based on .gitignore patterns - * @param {string[]} files - Array of file paths - * @param {string} rootDir - The root directory - * @returns {Promise} Filtered array of file paths - */ - -/** - * 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} project root directory or null if not found - */ - -const program = new Command(); - -program - .name('bmad-flatten') - .description('BMad-Method codebase flattener tool') - .version('1.0.0') - .option('-i, --input ', 'Input directory to flatten', process.cwd()) - .option('-o, --output ', '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 userSpecifiedOutput = argv.some((a) => a === '-o' || a === '--output' || a.startsWith('--output=')); - const noPathArguments = !userSpecifiedInput && !userSpecifiedOutput; - - if (noPathArguments) { - const detectedRoot = await findProjectRoot(process.cwd()); - const suggestedOutput = detectedRoot ? path.join(detectedRoot, 'flattened-codebase.xml') : path.resolve('flattened-codebase.xml'); - - if (detectedRoot) { - const useDefaults = await promptYesNo( - `Detected project root at "${detectedRoot}". Use it as input and write output to "${suggestedOutput}"?`, - true, - ); - if (useDefaults) { - inputDir = detectedRoot; - outputPath = suggestedOutput; - } else { - inputDir = await promptPath('Enter input directory path', process.cwd()); - outputPath = await promptPath('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()); - outputPath = await promptPath('Enter output file path', path.join(inputDir, 'flattened-codebase.xml')); - } - } - - // Ensure output directory exists - await fs.ensureDir(path.dirname(outputPath)); - - try { - // Verify input directory exists - 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'); - - // Start file discovery with spinner - 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`); - - // Process files with progress tracking - console.log('Reading file contents'); - const processingSpinner = ora('📄 Processing files...').start(); - const aggregatedContent = await aggregateFileContents(filteredFiles, inputDir, processingSpinner); - processingSpinner.succeed(`✅ Processed ${aggregatedContent.processedFiles}/${filteredFiles.length} files`); - if (aggregatedContent.errors.length > 0) { - console.log(`Errors: ${aggregatedContent.errors.length}`); - } - - // Generate XML output using streaming - const xmlSpinner = ora('🔧 Generating XML output...').start(); - await generateXMLOutput(aggregatedContent, outputPath); - xmlSpinner.succeed('📝 XML generation completed'); - - // Calculate and display statistics - const outputStats = await fs.stat(outputPath); - const stats = await calculateStatistics(aggregatedContent, outputStats.size, inputDir); - - // Display completion summary - console.log('\n📊 Completion Summary:'); - console.log(`✅ 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(`🔢 Estimated tokens: ${stats.estimatedTokens}`); - console.log(`📊 File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors\n`); - - // 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.'); - process.exit(1); - } - }); - -if (require.main === module) { - program.parse(); -} - -module.exports = program; diff --git a/tools/flattener/projectRoot.js b/tools/flattener/projectRoot.js deleted file mode 100644 index b2b9a7ae6..000000000 --- a/tools/flattener/projectRoot.js +++ /dev/null @@ -1,201 +0,0 @@ -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; -} - -/** - * 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. - * @param {string} startDir - * @returns {Promise} 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 }; - } - - 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)) }))); - - for (const { c, ok } of results) { - if (!ok) continue; - if (!best || c.weight >= best.weight) { - best = { dir, weight: c.weight }; - } - } - - if (dir === fsRoot) break; - dir = path.dirname(dir); - } - - const out = best ? best.dir : null; - _cache.set(startKey, out); - return out; - } catch { - return null; - } -} - -module.exports = { findProjectRoot }; diff --git a/tools/flattener/prompts.js b/tools/flattener/prompts.js deleted file mode 100644 index 849256d88..000000000 --- a/tools/flattener/prompts.js +++ /dev/null @@ -1,44 +0,0 @@ -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)); - return p; -} - -function createRl() { - return readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); -} - -function promptQuestion(question) { - return new Promise((resolve) => { - const rl = createRl(); - rl.question(question, (answer) => { - rl.close(); - resolve(answer); - }); - }); -} - -async function promptYesNo(question, defaultYes = true) { - 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; - return promptYesNo(question, defaultYes); -} - -async function promptPath(question, defaultValue) { - const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` : ''}: `; - const ans = (await promptQuestion(prompt)).trim(); - return expandHome(ans || defaultValue); -} - -module.exports = { promptYesNo, promptPath, promptQuestion, expandHome }; diff --git a/tools/flattener/stats.helpers.js b/tools/flattener/stats.helpers.js deleted file mode 100644 index 511bb0753..000000000 --- a/tools/flattener/stats.helpers.js +++ /dev/null @@ -1,368 +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 || ''; - 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, -}; diff --git a/tools/flattener/stats.js b/tools/flattener/stats.js deleted file mode 100644 index b41d50e57..000000000 --- a/tools/flattener/stats.js +++ /dev/null @@ -1,75 +0,0 @@ -const H = require('./stats.helpers.js'); - -async function calculateStatistics(aggregatedContent, xmlFileSize, rootDir) { - const { textFiles, binaryFiles, errors } = aggregatedContent; - - const totalLines = textFiles.reduce((sum, f) => sum + (f.lines || 0), 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); - - 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), - 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, - }; -} - -module.exports = { calculateStatistics }; diff --git a/tools/flattener/test-matrix.js b/tools/flattener/test-matrix.js deleted file mode 100644 index 0d9f6437e..000000000 --- a/tools/flattener/test-matrix.js +++ /dev/null @@ -1,409 +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'), '\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); -}); diff --git a/tools/flattener/xml.js b/tools/flattener/xml.js deleted file mode 100644 index 229f9a880..000000000 --- a/tools/flattener/xml.js +++ /dev/null @@ -1,82 +0,0 @@ -const fs = require('fs-extra'); -const { escapeXml } = require('../lib/xml-utils'); - -function indentFileContent(content) { - if (typeof content !== 'string') { - return String(content); - } - return content.split('\n').map((line) => ` ${line}`); -} - -function generateXMLOutput(aggregatedContent, outputPath) { - const { textFiles } = aggregatedContent; - const writeStream = fs.createWriteStream(outputPath, { encoding: 'utf8' }); - - return new Promise((resolve, reject) => { - writeStream.on('error', reject); - writeStream.on('finish', resolve); - - writeStream.write('\n'); - writeStream.write('\n'); - - // Sort files by path for deterministic order - const filesSorted = [...textFiles].sort((a, b) => a.path.localeCompare(b.path)); - let index = 0; - - const writeNext = () => { - if (index >= filesSorted.length) { - writeStream.write('\n'); - writeStream.end(); - return; - } - - const file = filesSorted[index++]; - const p = escapeXml(file.path); - const content = typeof file.content === 'string' ? file.content : ''; - - if (content.length === 0) { - writeStream.write(`\t\n`); - setTimeout(writeNext, 0); - return; - } - - 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" inside content, trim trailing newlines, indent each line with two tabs - const safe = content.replaceAll(']]>', ']]]]>'); - 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(']]>\n'); - } else { - // Write opening tag then newline; indent content with two tabs; attach closing tag directly after last content char - writeStream.write(`\t\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(`\n`); - } - - setTimeout(writeNext, 0); - }; - - writeNext(); - }); -} - -module.exports = { generateXMLOutput }; diff --git a/tools/schema/agent.js b/tools/schema/agent.js index 7d106e616..b6a36a985 100644 --- a/tools/schema/agent.js +++ b/tools/schema/agent.js @@ -210,7 +210,6 @@ function buildAgentSchema(expectedModule) { critical_actions: z.array(createNonEmptyString('agent.critical_actions[]')).optional(), menu: z.array(buildMenuItemSchema()).min(1, { message: 'agent.menu must include at least one entry' }), prompts: z.array(buildPromptSchema()).optional(), - webskip: z.boolean().optional(), discussion: z.boolean().optional(), conversational_knowledge: z.array(z.object({}).passthrough()).min(1).optional(), }) diff --git a/website/README.md b/website/README.md index 0545d295d..911c0447f 100644 --- a/website/README.md +++ b/website/README.md @@ -73,4 +73,3 @@ Note: If copying, remember to keep the copy in sync with changes to `docs/`. The build pipeline (`npm run docs:build`) produces: - Static HTML site in `build/site/` - LLM-friendly files: `llms.txt`, `llms-full.txt` -- Downloadable ZIP bundles in `downloads/` diff --git a/website/_basement/components/WorkflowGuide.astro b/website/_basement/components/WorkflowGuide.astro deleted file mode 100644 index d9dc7e197..000000000 --- a/website/_basement/components/WorkflowGuide.astro +++ /dev/null @@ -1,444 +0,0 @@ ---- ---- - -
-
- /bmad-help - Run this anytime to see what to do next — or ask it a question like "what should I do to build a web app?" -
- -

Loading agents is optional. If your IDE supports slash commands, you can run workflows directly.

- -
-
- - - -
-
- -
-

Select a track above to see the workflow.

- -
- -
Analysis
- -
- /brainstorm-project - Analyst - -

Guided ideation using 60+ techniques to explore your project idea and create brainstorm notes.

-
-
- -
- /research - Analyst - -

Market, technical, or competitive research producing a structured research document.

-
-
- -
- /product-brief - Analyst - -

Combines brainstorm and research into a foundation document covering problem, users, and MVP scope.

-
-
- - -
Planning
- -
- /quick-spec - Barry - -

Analyzes your codebase, auto-detects stack, and produces tech-spec.md with implementation-ready story files.

-
-
- -
- /create-prd - PM - -

Creates PRD.md with user personas, requirements, success metrics, and risks.

-
-
- -
- /create-ux-design - UX Designer - -

Creates ux-design.md with user journeys, wireframes, and a design system.

-
-
- - -
Solutioning
- -
- /create-architecture - Architect - -

Designs system architecture with ADRs covering data, API, security, and deployment decisions.

-
-
- -
- /create-epics-and-stories - PM - -

Breaks PRD and architecture into epic files with prioritized, technically-informed stories.

-
-
- -
- /implementation-readiness - Architect - -

Validates cohesion across all planning documents to confirm you're ready to build.

-
-
- - -
Implementation
- -
- /sprint-planning - SM - -

Initializes sprint-status.yaml to track all stories through development. Run once.

-
-
- - -
- ↻ Repeat for each story - -
- /create-story - SM - -

Prepares a story file with full context and acceptance criteria from the epic.

-
-
- -
- /dev-story - DEV - -

Implements production code and tests following architecture patterns.

-
-
- -
- /code-review - DEV - -

Reviews code for quality, architecture alignment, tests, and security.

-
-
-
- -
- /epic-retrospective - SM - -

Captures learnings from a completed epic to improve the next one.

-
-
-
-
- - - - diff --git a/website/_basement/pages/workflow-guide.astro b/website/_basement/pages/workflow-guide.astro deleted file mode 100644 index f9f929dfc..000000000 --- a/website/_basement/pages/workflow-guide.astro +++ /dev/null @@ -1,17 +0,0 @@ ---- -import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; -import WorkflowGuide from '../components/WorkflowGuide.astro'; ---- - - -

- This interactive guide helps you understand which workflows to run, which agents to use, and what outputs to expect at each phase. Select your project's track to see the relevant path. -

- -
diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 23a3179c4..d59de430a 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -4,7 +4,7 @@ import starlight from '@astrojs/starlight'; import sitemap from '@astrojs/sitemap'; import rehypeMarkdownLinks from './src/rehype-markdown-links.js'; import rehypeBasePaths from './src/rehype-base-paths.js'; -import { getSiteUrl } from './src/lib/site-url.js'; +import { getSiteUrl } from './src/lib/site-url.mjs'; const siteUrl = getSiteUrl(); const urlParts = new URL(siteUrl); diff --git a/website/src/components/Banner.astro b/website/src/components/Banner.astro index f1e460705..d0c94e5dc 100644 --- a/website/src/components/Banner.astro +++ b/website/src/components/Banner.astro @@ -1,12 +1,11 @@ --- -import { getSiteUrl } from '../lib/site-url.js'; +import { getSiteUrl } from '../lib/site-url.mjs'; -const SITE_URL = getSiteUrl(); -const fullDocsUrl = `${SITE_URL}/llms-full.txt`; +const llmsFullUrl = `${getSiteUrl()}/llms-full.txt`; ---
- 🤖 Consolidated, AI-optimized BMAD docs: llms-full.txt. Fetch this plain text file for complete context. + 🤖 Consolidated, AI-optimized BMAD docs: llms-full.txt. Fetch this plain text file for complete context.
diff --git a/website/src/lib/site-url.js b/website/src/lib/site-url.mjs similarity index 100% rename from website/src/lib/site-url.js rename to website/src/lib/site-url.mjs diff --git a/website/public/robots.txt b/website/src/pages/robots.txt.ts similarity index 61% rename from website/public/robots.txt rename to website/src/pages/robots.txt.ts index fc4884a79..d4dec7971 100644 --- a/website/public/robots.txt +++ b/website/src/pages/robots.txt.ts @@ -1,5 +1,10 @@ -# BMAD Method Documentation -# https://bmad-code-org.github.io/BMAD-METHOD/ +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = ({ site }) => { + const siteUrl = site?.href.replace(/\/$/, '') ?? ''; + + const body = `# BMAD Method Documentation +# ${siteUrl}/ # # This file controls web crawler access to the documentation site. @@ -34,4 +39,10 @@ User-agent: cohere-ai Allow: / # Sitemap -Sitemap: https://bmad-code-org.github.io/BMAD-METHOD/sitemap.xml +Sitemap: ${siteUrl}/sitemap-index.xml +`; + + return new Response(body, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); +}; diff --git a/website/src/rehype-base-paths.js b/website/src/rehype-base-paths.js index c61600912..317f07043 100644 --- a/website/src/rehype-base-paths.js +++ b/website/src/rehype-base-paths.js @@ -3,7 +3,6 @@ * * Transforms: * /img/foo.png → /BMAD-METHOD/img/foo.png (when base is /BMAD-METHOD/) - * /downloads/file.zip → /BMAD-METHOD/downloads/file.zip * /llms.txt → /BMAD-METHOD/llms.txt * * Only affects absolute paths (/) - relative paths and external URLs are unchanged.