diff --git a/CHANGELOG.md b/CHANGELOG.md
index e938d15b..a757f51f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
# Changelog
+## [6.0.0-alpha.19]
+
+**Release: December 18, 2025**
+
+### 🐛 Bug Fixes
+
+**Installer Stability:**
+
+- **Fixed \_bmad Folder Stutter**: Resolved issue with duplicate \_bmad folder creation when applying agent custom files
+- **Cleaner Installation**: Removed unnecessary backup file that was causing bloat in the installer
+- **Streamlined Agent Customization**: Fixed path handling for agent custom files to prevent folder duplication
+
+### 📊 Statistics
+
+- **3 files changed** with critical fix
+- **3,688 lines removed** by eliminating backup files
+- **Improved installer performance** and stability
+
+---
+
## [6.0.0-alpha.18]
**Release: December 18, 2025**
diff --git a/README.md b/README.md
index e23f6bb6..379e75ef 100644
--- a/README.md
+++ b/README.md
@@ -292,6 +292,8 @@ MIT License - See [LICENSE](LICENSE) for details.
**Trademarks:** BMad™ and BMAD-METHOD™ are trademarks of BMad Code, LLC.
+Supported by:
+
---
diff --git a/package.json b/package.json
index 738aba72..f9694ae6 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-alpha.18",
+ "version": "6.0.0-alpha.19",
"description": "Breakthrough Method of Agile AI-driven Development",
"keywords": [
"agile",
diff --git a/src/modules/bmgd/agents/game-architect.agent.yaml b/src/modules/bmgd/agents/game-architect.agent.yaml
index 8e218901..3b28024d 100644
--- a/src/modules/bmgd/agents/game-architect.agent.yaml
+++ b/src/modules/bmgd/agents/game-architect.agent.yaml
@@ -33,6 +33,10 @@ agent:
exec: "{project-root}/_bmad/bmgd/workflows/3-technical/game-architecture/workflow.md"
description: Produce a Scale Adaptive Game Architecture
+ - trigger: generate-project-context
+ exec: "{project-root}/_bmad/bmgd/workflows/3-technical/generate-project-context/workflow.md"
+ description: Create optimized project-context.md for AI agent consistency
+
- trigger: correct-course
workflow: "{project-root}/_bmad/bmgd/workflows/4-production/correct-course/workflow.yaml"
description: Course Correction Analysis (when implementation is off-track)
diff --git a/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-09-complete.md b/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-09-complete.md
index 7e71161e..51f022e3 100644
--- a/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-09-complete.md
+++ b/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-09-complete.md
@@ -12,6 +12,7 @@ outputFile: '{output_folder}/game-architecture.md'
# Handoff References
epicWorkflow: '{project-root}/_bmad/bmgd/workflows/4-production/epic-workflow/workflow.yaml'
+projectContextWorkflow: '{project-root}/_bmad/bmgd/workflows/3-technical/generate-project-context/workflow.md'
---
# Step 9: Completion
@@ -131,7 +132,17 @@ platform: '{{platform}}'
---
````
-### 4. Present Completion Summary
+### 4. Update Workflow Status
+
+**If not in standalone mode:**
+
+Load `{output_folder}/bmgd-workflow-status.yaml` and:
+
+- Update `create-architecture` status to the output file path
+- Preserve all comments and structure
+- Determine next workflow in sequence
+
+### 5. Present Completion Summary
"**Architecture Complete!**
@@ -158,9 +169,50 @@ platform: '{{platform}}'
**Document saved to:** `{outputFile}`
-Do you want to review or adjust anything before we finalize?"
+Do you want to review or adjust anything before we finalize?
-### 5. Handle Review Requests
+**Optional Enhancement: Project Context File**
+
+Would you like to create a `project-context.md` file? This is a concise, optimized guide for AI agents that captures:
+
+- Critical engine-specific rules they might miss
+- Specific patterns and conventions for your game project
+- Performance and optimization requirements
+- Anti-patterns and edge cases to avoid
+
+{if_existing_project_context}
+I noticed you already have a project context file. Would you like to update it with your new architectural decisions?
+{else}
+This file helps ensure AI agents implement game code consistently with your project's unique requirements and patterns.
+{/if_existing_project_context}
+
+**Create/Update project context?** [Y/N]"
+
+### 6. Handle Project Context Creation Choice
+
+If user responds 'Y' or 'yes' to creating/updating project context:
+
+"Excellent choice! Let me launch the Generate Project Context workflow to create a comprehensive guide for AI agents.
+
+This will help ensure consistent implementation by capturing:
+
+- Engine-specific patterns and rules
+- Performance and optimization conventions from your architecture
+- Testing and quality standards
+- Anti-patterns to avoid
+
+The workflow will collaborate with you to create an optimized `project-context.md` file that AI agents will read before implementing any game code."
+
+**Execute the Generate Project Context workflow:**
+
+- Load and execute: `{projectContextWorkflow}`
+- The workflow will handle discovery, generation, and completion of the project context file
+- After completion, return here for final handoff
+
+If user responds 'N' or 'no':
+"Understood! Your architecture is complete and ready for implementation. You can always create a project context file later using the Generate Project Context workflow if needed."
+
+### 7. Handle Review Requests
**If user wants to review:**
@@ -179,7 +231,7 @@ Or type 'all' to see the complete document."
**Show requested section and allow edits.**
-### 6. Present Next Steps Menu
+### 8. Present Next Steps Menu
**After user confirms completion:**
@@ -204,7 +256,7 @@ Or type 'all' to see the complete document."
2. Proceed to Epic creation workflow
3. Exit workflow"
-### 7. Handle User Selection
+### 9. Handle User Selection
Based on user choice:
@@ -224,7 +276,7 @@ Based on user choice:
- Confirm document is saved and complete
- Exit workflow gracefully
-### 8. Provide Handoff Guidance
+### 10. Provide Handoff Guidance
**For Epic Creation handoff:**
@@ -270,6 +322,7 @@ This is the final step. Ensure:
- Development setup is complete
- Document status updated to 'complete'
- Frontmatter shows all steps completed
+- Workflow status updated (if tracking)
- User has clear next steps
- Document saved and ready for AI agent consumption
@@ -278,6 +331,7 @@ This is the final step. Ensure:
- Missing executive summary
- Incomplete development setup
- Frontmatter not updated
+- Status not updated when tracking
- No clear next steps provided
- User left without actionable guidance
diff --git a/src/modules/bmgd/workflows/3-technical/generate-project-context/project-context-template.md b/src/modules/bmgd/workflows/3-technical/generate-project-context/project-context-template.md
new file mode 100644
index 00000000..b9e4d3bc
--- /dev/null
+++ b/src/modules/bmgd/workflows/3-technical/generate-project-context/project-context-template.md
@@ -0,0 +1,20 @@
+---
+project_name: '{{project_name}}'
+user_name: '{{user_name}}'
+date: '{{date}}'
+sections_completed: []
+---
+
+# Project Context for AI Agents
+
+_This file contains critical rules and patterns that AI agents must follow when implementing game code in this project. Focus on unobvious details that agents might otherwise miss._
+
+---
+
+## Technology Stack & Versions
+
+_Documented after discovery phase_
+
+## Critical Implementation Rules
+
+_Documented after discovery phase_
diff --git a/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-01-discover.md b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-01-discover.md
new file mode 100644
index 00000000..a92db901
--- /dev/null
+++ b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-01-discover.md
@@ -0,0 +1,201 @@
+# Step 1: Context Discovery & Initialization
+
+## MANDATORY EXECUTION RULES (READ FIRST):
+
+- NEVER generate content without user input
+- ALWAYS treat this as collaborative discovery between technical peers
+- YOU ARE A FACILITATOR, not a content generator
+- FOCUS on discovering existing project context and technology stack
+- IDENTIFY critical implementation rules that AI agents need
+- ABSOLUTELY NO TIME ESTIMATES
+
+## EXECUTION PROTOCOLS:
+
+- Show your analysis before taking any action
+- Read existing project files to understand current context
+- Initialize document and update frontmatter
+- FORBIDDEN to load next step until discovery is complete
+
+## CONTEXT BOUNDARIES:
+
+- Variables from workflow.md are available in memory
+- Focus on existing project files and architecture decisions
+- Look for patterns, conventions, and unique requirements
+- Prioritize rules that prevent implementation mistakes
+
+## YOUR TASK:
+
+Discover the project's game engine, technology stack, existing patterns, and critical implementation rules that AI agents must follow when writing game code.
+
+## DISCOVERY SEQUENCE:
+
+### 1. Check for Existing Project Context
+
+First, check if project context already exists:
+
+- Look for file at `{output_folder}/project-context.md`
+- If exists: Read complete file to understand existing rules
+- Present to user: "Found existing project context with {number_of_sections} sections. Would you like to update this or create a new one?"
+
+### 2. Discover Game Engine & Technology Stack
+
+Load and analyze project files to identify technologies:
+
+**Architecture Document:**
+
+- Look for `{output_folder}/game-architecture.md` or `{output_folder}/architecture.md`
+- Extract engine choice with specific version (Unity, Unreal, Godot, custom)
+- Note architectural decisions that affect implementation
+
+**Engine-Specific Files:**
+
+- Unity: Check for `ProjectSettings/ProjectVersion.txt`, `Packages/manifest.json`
+- Unreal: Check for `.uproject` files, `Config/DefaultEngine.ini`
+- Godot: Check for `project.godot`, `export_presets.cfg`
+- Custom: Check for engine config files, build scripts
+
+**Package/Dependency Files:**
+
+- Unity: `Packages/manifest.json`, NuGet packages
+- Unreal: `.Build.cs` files, plugin configs
+- Godot: `addons/` directory, GDExtension configs
+- Web-based: `package.json`, `requirements.txt`
+
+**Configuration Files:**
+
+- Build tool configs
+- Linting and formatting configs
+- Testing configurations
+- CI/CD pipeline configs
+
+### 3. Identify Existing Code Patterns
+
+Search through existing codebase for patterns:
+
+**Naming Conventions:**
+
+- Script/class naming patterns
+- Asset naming conventions
+- Scene/level naming patterns
+- Test file naming patterns
+
+**Code Organization:**
+
+- How components/scripts are structured
+- Where utilities and helpers are placed
+- How systems are organized
+- Folder hierarchy patterns
+
+**Engine-Specific Patterns:**
+
+- Unity: MonoBehaviour patterns, ScriptableObject usage, serialization rules
+- Unreal: Actor/Component patterns, Blueprint integration, UE macros
+- Godot: Node patterns, signal usage, autoload patterns
+
+### 4. Extract Critical Implementation Rules
+
+Look for rules that AI agents might miss:
+
+**Engine-Specific Rules:**
+
+- Unity: Assembly definitions, Unity lifecycle methods, coroutine patterns
+- Unreal: UPROPERTY/UFUNCTION usage, garbage collection rules, tick patterns
+- Godot: `_ready` vs `_enter_tree`, node ownership, scene instancing
+
+**Performance Rules:**
+
+- Frame budget constraints
+- Memory allocation patterns
+- Hot path optimization requirements
+- Object pooling patterns
+
+**Platform-Specific Rules:**
+
+- Target platform constraints
+- Input handling conventions
+- Platform-specific code patterns
+- Build configuration rules
+
+**Testing Rules:**
+
+- Test structure requirements
+- Mock usage conventions
+- Integration vs unit test boundaries
+- Play mode vs edit mode testing
+
+### 5. Initialize Project Context Document
+
+Based on discovery, create or update the context document:
+
+#### A. Fresh Document Setup (if no existing context)
+
+Copy template from `{installed_path}/project-context-template.md` to `{output_folder}/project-context.md`
+Initialize frontmatter with:
+
+```yaml
+---
+project_name: '{{project_name}}'
+user_name: '{{user_name}}'
+date: '{{date}}'
+sections_completed: ['technology_stack']
+existing_patterns_found: { { number_of_patterns_discovered } }
+---
+```
+
+#### B. Existing Document Update
+
+Load existing context and prepare for updates
+Set frontmatter `sections_completed` to track what will be updated
+
+### 6. Present Discovery Summary
+
+Report findings to user:
+
+"Welcome {{user_name}}! I've analyzed your game project for {{project_name}} to discover the context that AI agents need.
+
+**Game Engine & Stack Discovered:**
+{{engine_and_version}}
+{{list_of_technologies_with_versions}}
+
+**Existing Patterns Found:**
+
+- {{number_of_patterns}} implementation patterns
+- {{number_of_conventions}} coding conventions
+- {{number_of_rules}} critical rules
+
+**Key Areas for Context Rules:**
+
+- {{area_1}} (e.g., Engine lifecycle and patterns)
+- {{area_2}} (e.g., Performance and optimization)
+- {{area_3}} (e.g., Platform-specific requirements)
+
+{if_existing_context}
+**Existing Context:** Found {{sections}} sections already defined. We can update or add to these.
+{/if_existing_context}
+
+Ready to create/update your project context. This will help AI agents implement game code consistently with your project's standards.
+
+[C] Continue to context generation"
+
+## SUCCESS METRICS:
+
+- Existing project context properly detected and handled
+- Game engine and technology stack accurately identified with versions
+- Critical implementation patterns discovered
+- Project context document properly initialized
+- Discovery findings clearly presented to user
+- User ready to proceed with context generation
+
+## FAILURE MODES:
+
+- Not checking for existing project context before creating new one
+- Missing critical engine versions or configurations
+- Overlooking important coding patterns or conventions
+- Not initializing frontmatter properly
+- Not presenting clear discovery summary to user
+
+## NEXT STEP:
+
+After user selects [C] to continue, load `./step-02-generate.md` to collaboratively generate the specific project context rules.
+
+Remember: Do NOT proceed to step-02 until user explicitly selects [C] from the menu and discovery is confirmed!
diff --git a/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-02-generate.md b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-02-generate.md
new file mode 100644
index 00000000..75e978cb
--- /dev/null
+++ b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-02-generate.md
@@ -0,0 +1,373 @@
+# Step 2: Context Rules Generation
+
+## MANDATORY EXECUTION RULES (READ FIRST):
+
+- NEVER generate content without user input
+- ALWAYS treat this as collaborative discovery between technical peers
+- YOU ARE A FACILITATOR, not a content generator
+- FOCUS on unobvious rules that AI agents need to be reminded of
+- KEEP CONTENT LEAN - optimize for LLM context efficiency
+- ABSOLUTELY NO TIME ESTIMATES
+
+## EXECUTION PROTOCOLS:
+
+- Show your analysis before taking any action
+- Focus on specific, actionable rules rather than general advice
+- Present A/P/C menu after each major rule category
+- ONLY save when user chooses C (Continue)
+- Update frontmatter with completed sections
+- FORBIDDEN to load next step until all sections are complete
+
+## COLLABORATION MENUS (A/P/C):
+
+This step will generate content and present choices for each rule category:
+
+- **A (Advanced Elicitation)**: Use discovery protocols to explore nuanced implementation rules
+- **P (Party Mode)**: Bring multiple perspectives to identify critical edge cases
+- **C (Continue)**: Save the current rules and proceed to next category
+
+## PROTOCOL INTEGRATION:
+
+- When 'A' selected: Execute {project-root}/\_bmad/core/tasks/advanced-elicitation.xml
+- When 'P' selected: Execute {project-root}/\_bmad/core/workflows/party-mode
+- PROTOCOLS always return to display this step's A/P/C menu after the A or P have completed
+- User accepts/rejects protocol changes before proceeding
+
+## CONTEXT BOUNDARIES:
+
+- Discovery results from step-1 are available
+- Game engine and existing patterns are identified
+- Focus on rules that prevent implementation mistakes
+- Prioritize unobvious details that AI agents might miss
+
+## YOUR TASK:
+
+Collaboratively generate specific, critical rules that AI agents must follow when implementing game code in this project.
+
+## CONTEXT GENERATION SEQUENCE:
+
+### 1. Technology Stack & Versions
+
+Document the exact technology stack from discovery:
+
+**Core Technologies:**
+Based on user skill level, present findings:
+
+**Expert Mode:**
+"Technology stack from your architecture and project files:
+{{exact_technologies_with_versions}}
+
+Any critical version constraints I should document for agents?"
+
+**Intermediate Mode:**
+"I found your technology stack:
+
+**Game Engine:**
+{{engine_with_version}}
+
+**Key Dependencies:**
+{{important_dependencies_with_versions}}
+
+Are there any version constraints or compatibility notes agents should know about?"
+
+**Beginner Mode:**
+"Here are the technologies you're using:
+
+**Game Engine:**
+{{friendly_description_of_engine}}
+
+**Important Notes:**
+{{key_things_agents_need_to_know_about_versions}}
+
+Should I document any special version rules or compatibility requirements?"
+
+### 2. Engine-Specific Rules
+
+Focus on unobvious engine patterns agents might miss:
+
+**Unity Rules (if applicable):**
+"Based on your Unity project, I notice some specific patterns:
+
+**Lifecycle Rules:**
+{{unity_lifecycle_patterns}}
+
+**Serialization Rules:**
+{{serialization_requirements}}
+
+**Assembly Definitions:**
+{{assembly_definition_rules}}
+
+**Coroutine/Async Patterns:**
+{{async_patterns}}
+
+Are these patterns correct? Any other Unity-specific rules agents should follow?"
+
+**Unreal Rules (if applicable):**
+"Based on your Unreal project, I notice some specific patterns:
+
+**UPROPERTY/UFUNCTION Rules:**
+{{macro_usage_patterns}}
+
+**Blueprint Integration:**
+{{blueprint_rules}}
+
+**Garbage Collection:**
+{{gc_patterns}}
+
+**Tick Patterns:**
+{{tick_optimization_rules}}
+
+Are these patterns correct? Any other Unreal-specific rules agents should follow?"
+
+**Godot Rules (if applicable):**
+"Based on your Godot project, I notice some specific patterns:
+
+**Node Lifecycle:**
+{{node_lifecycle_patterns}}
+
+**Signal Usage:**
+{{signal_conventions}}
+
+**Scene Instancing:**
+{{scene_patterns}}
+
+**Autoload Patterns:**
+{{autoload_rules}}
+
+Are these patterns correct? Any other Godot-specific rules agents should follow?"
+
+### 3. Performance Rules
+
+Document performance-critical patterns:
+
+**Frame Budget Rules:**
+"Your game has these performance requirements:
+
+**Target Frame Rate:**
+{{target_fps}}
+
+**Frame Budget:**
+{{milliseconds_per_frame}}
+
+**Critical Systems:**
+{{systems_that_must_meet_budget}}
+
+**Hot Path Rules:**
+{{hot_path_patterns}}
+
+Any other performance rules agents must follow?"
+
+**Memory Management:**
+"Memory patterns for your project:
+
+**Allocation Rules:**
+{{allocation_patterns}}
+
+**Pooling Requirements:**
+{{object_pooling_rules}}
+
+**Asset Loading:**
+{{asset_loading_patterns}}
+
+Are there memory constraints agents should know about?"
+
+### 4. Code Organization Rules
+
+Document project structure and organization:
+
+**Folder Structure:**
+"Your project organization:
+
+**Script Organization:**
+{{script_folder_structure}}
+
+**Asset Organization:**
+{{asset_folder_patterns}}
+
+**Scene/Level Organization:**
+{{scene_organization}}
+
+Any organization rules agents must follow?"
+
+**Naming Conventions:**
+"Your naming patterns:
+
+**Script/Class Names:**
+{{class_naming_patterns}}
+
+**Asset Names:**
+{{asset_naming_patterns}}
+
+**Variable/Method Names:**
+{{variable_naming_patterns}}
+
+Any other naming rules?"
+
+### 5. Testing Rules
+
+Focus on testing patterns that ensure consistency:
+
+**Test Structure Rules:**
+"Your testing setup shows these patterns:
+
+**Test Organization:**
+{{test_file_organization}}
+
+**Test Categories:**
+{{unit_vs_integration_boundaries}}
+
+**Mocking Patterns:**
+{{mock_usage_conventions}}
+
+**Play Mode Testing:**
+{{play_mode_test_patterns}}
+
+Are there testing rules agents should always follow?"
+
+### 6. Platform & Build Rules
+
+Document platform-specific requirements:
+
+**Target Platforms:**
+"Your platform configuration:
+
+**Primary Platform:**
+{{primary_platform}}
+
+**Platform-Specific Code:**
+{{platform_conditional_patterns}}
+
+**Build Configurations:**
+{{build_config_rules}}
+
+**Input Handling:**
+{{input_abstraction_patterns}}
+
+Any platform rules agents must know?"
+
+### 7. Critical Don't-Miss Rules
+
+Identify rules that prevent common mistakes:
+
+**Anti-Patterns to Avoid:**
+"Based on your codebase, here are critical things agents must NOT do:
+
+{{critical_anti_patterns_with_examples}}
+
+**Edge Cases:**
+{{specific_edge_cases_agents_should_handle}}
+
+**Common Gotchas:**
+{{engine_specific_gotchas}}
+
+**Performance Traps:**
+{{performance_patterns_to_avoid}}
+
+Are there other 'gotchas' agents should know about?"
+
+### 8. Generate Context Content
+
+For each category, prepare lean content for the project context file:
+
+#### Content Structure:
+
+```markdown
+## Technology Stack & Versions
+
+{{concise_technology_list_with_exact_versions}}
+
+## Critical Implementation Rules
+
+### Engine-Specific Rules
+
+{{bullet_points_of_engine_rules}}
+
+### Performance Rules
+
+{{bullet_points_of_performance_requirements}}
+
+### Code Organization Rules
+
+{{bullet_points_of_organization_patterns}}
+
+### Testing Rules
+
+{{bullet_points_of_testing_requirements}}
+
+### Platform & Build Rules
+
+{{bullet_points_of_platform_requirements}}
+
+### Critical Don't-Miss Rules
+
+{{bullet_points_of_anti_patterns_and_gotchas}}
+```
+
+### 9. Present Content and Menu
+
+After each category, show the generated rules and present choices:
+
+"I've drafted the {{category_name}} rules for your project context.
+
+**Here's what I'll add:**
+
+[Show the complete markdown content for this category]
+
+**What would you like to do?**
+[A] Advanced Elicitation - Explore nuanced rules for this category
+[P] Party Mode - Review from different implementation perspectives
+[C] Continue - Save these rules and move to next category"
+
+### 10. Handle Menu Selection
+
+#### If 'A' (Advanced Elicitation):
+
+- Execute advanced-elicitation.xml with current category rules
+- Process enhanced rules that come back
+- Ask user: "Accept these enhanced rules for {{category}}? (y/n)"
+- If yes: Update content, then return to A/P/C menu
+- If no: Keep original content, then return to A/P/C menu
+
+#### If 'P' (Party Mode):
+
+- Execute party-mode workflow with category rules context
+- Process collaborative insights on implementation patterns
+- Ask user: "Accept these changes to {{category}} rules? (y/n)"
+- If yes: Update content, then return to A/P/C menu
+- If no: Keep original content, then return to A/P/C menu
+
+#### If 'C' (Continue):
+
+- Save the current category content to project context file
+- Update frontmatter: `sections_completed: [...]`
+- Proceed to next category or step-03 if complete
+
+## APPEND TO PROJECT CONTEXT:
+
+When user selects 'C' for a category, append the content directly to `{output_folder}/project-context.md` using the structure from step 8.
+
+## SUCCESS METRICS:
+
+- All critical technology versions accurately documented
+- Engine-specific rules cover unobvious patterns
+- Performance rules capture project-specific requirements
+- Code organization rules maintain project standards
+- Testing rules ensure consistent test quality
+- Platform rules prevent cross-platform issues
+- Content is lean and optimized for LLM context
+- A/P/C menu presented and handled correctly for each category
+
+## FAILURE MODES:
+
+- Including obvious rules that agents already know
+- Making content too verbose for LLM context efficiency
+- Missing critical anti-patterns or edge cases
+- Not getting user validation for each rule category
+- Not documenting exact versions and configurations
+- Not presenting A/P/C menu after content generation
+
+## NEXT STEP:
+
+After completing all rule categories and user selects 'C' for the final category, load `./step-03-complete.md` to finalize the project context file.
+
+Remember: Do NOT proceed to step-03 until all categories are complete and user explicitly selects 'C' for each!
diff --git a/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-03-complete.md b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-03-complete.md
new file mode 100644
index 00000000..e87e1382
--- /dev/null
+++ b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-03-complete.md
@@ -0,0 +1,279 @@
+# Step 3: Context Completion & Finalization
+
+## MANDATORY EXECUTION RULES (READ FIRST):
+
+- NEVER generate content without user input
+- ALWAYS treat this as collaborative completion between technical peers
+- YOU ARE A FACILITATOR, not a content generator
+- FOCUS on finalizing a lean, LLM-optimized project context
+- ENSURE all critical rules are captured and actionable
+- ABSOLUTELY NO TIME ESTIMATES
+
+## EXECUTION PROTOCOLS:
+
+- Show your analysis before taking any action
+- Review and optimize content for LLM context efficiency
+- Update frontmatter with completion status
+- NO MORE STEPS - this is the final step
+
+## CONTEXT BOUNDARIES:
+
+- All rule categories from step-2 are complete
+- Technology stack and versions are documented
+- Focus on final review, optimization, and completion
+- Ensure the context file is ready for AI agent consumption
+
+## YOUR TASK:
+
+Complete the project context file, optimize it for LLM efficiency, and provide guidance for usage and maintenance.
+
+## COMPLETION SEQUENCE:
+
+### 1. Review Complete Context File
+
+Read the entire project context file and analyze:
+
+**Content Analysis:**
+
+- Total length and readability for LLMs
+- Clarity and specificity of rules
+- Coverage of all critical areas
+- Actionability of each rule
+
+**Structure Analysis:**
+
+- Logical organization of sections
+- Consistency of formatting
+- Absence of redundant or obvious information
+- Optimization for quick scanning
+
+### 2. Optimize for LLM Context
+
+Ensure the file is lean and efficient:
+
+**Content Optimization:**
+
+- Remove any redundant rules or obvious information
+- Combine related rules into concise bullet points
+- Use specific, actionable language
+- Ensure each rule provides unique value
+
+**Formatting Optimization:**
+
+- Use consistent markdown formatting
+- Implement clear section hierarchy
+- Ensure scannability with strategic use of bolding
+- Maintain readability while maximizing information density
+
+### 3. Final Content Structure
+
+Ensure the final structure follows this optimized format:
+
+```markdown
+# Project Context for AI Agents
+
+_This file contains critical rules and patterns that AI agents must follow when implementing game code in this project. Focus on unobvious details that agents might otherwise miss._
+
+---
+
+## Technology Stack & Versions
+
+{{concise_technology_list}}
+
+## Critical Implementation Rules
+
+### Engine-Specific Rules
+
+{{engine_rules}}
+
+### Performance Rules
+
+{{performance_requirements}}
+
+### Code Organization Rules
+
+{{organization_patterns}}
+
+### Testing Rules
+
+{{testing_requirements}}
+
+### Platform & Build Rules
+
+{{platform_requirements}}
+
+### Critical Don't-Miss Rules
+
+{{anti_patterns_and_gotchas}}
+
+---
+
+## Usage Guidelines
+
+**For AI Agents:**
+
+- Read this file before implementing any game code
+- Follow ALL rules exactly as documented
+- When in doubt, prefer the more restrictive option
+- Update this file if new patterns emerge
+
+**For Humans:**
+
+- Keep this file lean and focused on agent needs
+- Update when technology stack changes
+- Review quarterly for outdated rules
+- Remove rules that become obvious over time
+
+Last Updated: {{date}}
+```
+
+### 4. Present Completion Summary
+
+Based on user skill level, present the completion:
+
+**Expert Mode:**
+"Project context complete. Optimized for LLM consumption with {{rule_count}} critical rules across {{section_count}} sections.
+
+File saved to: `{output_folder}/project-context.md`
+
+Ready for AI agent integration."
+
+**Intermediate Mode:**
+"Your project context is complete and optimized for AI agents!
+
+**What we created:**
+
+- {{rule_count}} critical implementation rules
+- Technology stack with exact versions
+- Engine-specific patterns and conventions
+- Performance and optimization guidelines
+- Testing and platform requirements
+
+**Key benefits:**
+
+- AI agents will implement consistently with your standards
+- Reduced context switching and implementation errors
+- Clear guidance for unobvious project requirements
+
+**Next steps:**
+
+- AI agents should read this file before implementing
+- Update as your project evolves
+- Review periodically for optimization"
+
+**Beginner Mode:**
+"Excellent! Your project context guide is ready!
+
+**What this does:**
+Think of this as a 'rules of the road' guide for AI agents working on your game. It ensures they all follow the same patterns and avoid common mistakes.
+
+**What's included:**
+
+- Exact engine and technology versions to use
+- Critical coding rules they might miss
+- Performance and optimization standards
+- Testing and platform requirements
+
+**How AI agents use it:**
+They read this file before writing any code, ensuring everything they create follows your project's standards perfectly.
+
+Your project context is saved and ready to help agents implement consistently!"
+
+### 5. Final File Updates
+
+Update the project context file with completion information:
+
+**Frontmatter Update:**
+
+```yaml
+---
+project_name: '{{project_name}}'
+user_name: '{{user_name}}'
+date: '{{date}}'
+sections_completed:
+ ['technology_stack', 'engine_rules', 'performance_rules', 'organization_rules', 'testing_rules', 'platform_rules', 'anti_patterns']
+status: 'complete'
+rule_count: { { total_rules } }
+optimized_for_llm: true
+---
+```
+
+**Add Usage Section:**
+Append the usage guidelines from step 3 to complete the document.
+
+### 6. Completion Validation
+
+Final checks before completion:
+
+**Content Validation:**
+
+- All critical technology versions documented
+- Engine-specific rules are specific and actionable
+- Performance rules capture project requirements
+- Code organization rules maintain standards
+- Testing rules ensure consistency
+- Platform rules prevent cross-platform issues
+- Anti-pattern rules prevent common mistakes
+
+**Format Validation:**
+
+- Content is lean and optimized for LLMs
+- Structure is logical and scannable
+- No redundant or obvious information
+- Consistent formatting throughout
+
+### 7. Completion Message
+
+Present final completion to user:
+
+"**Project Context Generation Complete!**
+
+Your optimized project context file is ready at:
+`{output_folder}/project-context.md`
+
+**Context Summary:**
+
+- {{rule_count}} critical rules for AI agents
+- {{section_count}} comprehensive sections
+- Optimized for LLM context efficiency
+- Ready for immediate agent integration
+
+**Key Benefits:**
+
+- Consistent implementation across all AI agents
+- Reduced common mistakes and edge cases
+- Clear guidance for project-specific patterns
+- Minimal LLM context usage
+
+**Next Steps:**
+
+1. AI agents will automatically read this file when implementing
+2. Update this file when your technology stack or patterns evolve
+3. Review quarterly to optimize and remove outdated rules
+
+Your project context will help ensure high-quality, consistent game implementation across all development work. Great work capturing your project's critical implementation requirements!"
+
+## SUCCESS METRICS:
+
+- Complete project context file with all critical rules
+- Content optimized for LLM context efficiency
+- All technology versions and patterns documented
+- File structure is logical and scannable
+- Usage guidelines included for agents and humans
+- Frontmatter properly updated with completion status
+- User provided with clear next steps and benefits
+
+## FAILURE MODES:
+
+- Final content is too verbose for LLM consumption
+- Missing critical implementation rules or patterns
+- Not optimizing content for agent readability
+- Not providing clear usage guidelines
+- Frontmatter not properly updated
+- Not validating file completion before ending
+
+## WORKFLOW COMPLETE:
+
+This is the final step of the Generate Project Context workflow. The user now has a comprehensive, optimized project context file that will ensure consistent, high-quality game implementation across all AI agents working on the project.
+
+The project context file serves as the critical "rules of the road" that agents need to implement game code consistently with the project's standards and patterns.
diff --git a/src/modules/bmgd/workflows/3-technical/generate-project-context/workflow.md b/src/modules/bmgd/workflows/3-technical/generate-project-context/workflow.md
new file mode 100644
index 00000000..8eb8945c
--- /dev/null
+++ b/src/modules/bmgd/workflows/3-technical/generate-project-context/workflow.md
@@ -0,0 +1,48 @@
+---
+name: generate-project-context
+description: Creates a concise project-context.md file with critical rules and patterns that AI agents must follow when implementing game code. Optimized for LLM context efficiency.
+---
+
+# Generate Project Context Workflow
+
+**Goal:** Create a concise, optimized `project-context.md` file containing critical rules, patterns, and guidelines that AI agents must follow when implementing game code. This file focuses on unobvious details that LLMs need to be reminded of.
+
+**Your Role:** You are a technical facilitator working with a peer to capture the essential implementation rules that will ensure consistent, high-quality game code generation across all AI agents working on the project.
+
+---
+
+## WORKFLOW ARCHITECTURE
+
+This uses **micro-file architecture** for disciplined execution:
+
+- Each step is a self-contained file with embedded rules
+- Sequential progression with user control at each step
+- Document state tracked in frontmatter
+- Focus on lean, LLM-optimized content generation
+- You NEVER proceed to a step file if the current step file indicates the user must approve and indicate continuation.
+
+---
+
+## INITIALIZATION
+
+### Configuration Loading
+
+Load config from `{project-root}/_bmad/bmgd/config.yaml` and resolve:
+
+- `project_name`, `output_folder`, `user_name`
+- `communication_language`, `document_output_language`, `game_dev_experience`
+- `date` as system-generated current datetime
+
+### Paths
+
+- `installed_path` = `{project-root}/_bmad/bmgd/workflows/3-technical/generate-project-context`
+- `template_path` = `{installed_path}/project-context-template.md`
+- `output_file` = `{output_folder}/project-context.md`
+
+---
+
+## EXECUTION
+
+Load and execute `steps/step-01-discover.md` to begin the workflow.
+
+**Note:** Input document discovery and initialization protocols are handled in step-01-discover.md.
diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js
index e99dca82..4c675121 100644
--- a/tools/cli/installers/lib/core/installer.js
+++ b/tools/cli/installers/lib/core/installer.js
@@ -13,12 +13,13 @@ const { XmlHandler } = require('../../../lib/xml-handler');
const { DependencyResolver } = require('./dependency-resolver');
const { ConfigCollector } = require('./config-collector');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
-const { AgentPartyGenerator } = require('../../../lib/agent-party-generator');
const { CLIUtils } = require('../../../lib/cli-utils');
const { ManifestGenerator } = require('./manifest-generator');
const { IdeConfigManager } = require('./ide-config-manager');
const { CustomHandler } = require('../custom/handler');
-const { filterCustomizationData } = require('../../../lib/agent/compiler');
+
+// BMAD installation folder name - this is constant and should never change
+const BMAD_FOLDER_NAME = '_bmad';
class Installer {
constructor() {
@@ -34,58 +35,35 @@ class Installer {
this.ideConfigManager = new IdeConfigManager();
this.installedFiles = new Set(); // Track all installed files
this.ttsInjectedFiles = []; // Track files with TTS injection applied
+ this.bmadFolderName = BMAD_FOLDER_NAME;
}
/**
* Find the bmad installation directory in a project
- * V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml
+ * Always uses the standard _bmad folder name
* Also checks for legacy _cfg folder for migration
* @param {string} projectDir - Project directory
* @returns {Promise} { bmadDir: string, hasLegacyCfg: boolean }
*/
async findBmadDir(projectDir) {
+ const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
+
// Check if project directory exists
if (!(await fs.pathExists(projectDir))) {
// Project doesn't exist yet, return default
- return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
+ return { bmadDir, hasLegacyCfg: false };
}
- let bmadDir = null;
+ // Check for legacy _cfg folder if bmad directory exists
let hasLegacyCfg = false;
-
- try {
- const entries = await fs.readdir(projectDir, { withFileTypes: true });
- for (const entry of entries) {
- if (entry.isDirectory()) {
- const bmadPath = path.join(projectDir, entry.name);
-
- // Check for current _config folder
- const manifestPath = path.join(bmadPath, '_config', 'manifest.yaml');
- if (await fs.pathExists(manifestPath)) {
- // Found a V6+ installation with current _config folder
- return { bmadDir: bmadPath, hasLegacyCfg: false };
- }
-
- // Check for legacy _cfg folder
- const legacyManifestPath = path.join(bmadPath, '_cfg', 'manifest.yaml');
- if (await fs.pathExists(legacyManifestPath)) {
- bmadDir = bmadPath;
- hasLegacyCfg = true;
- }
- }
+ if (await fs.pathExists(bmadDir)) {
+ const legacyCfgPath = path.join(bmadDir, '_cfg');
+ if (await fs.pathExists(legacyCfgPath)) {
+ hasLegacyCfg = true;
}
- } catch {
- console.log(chalk.red('Error reading project directory for BMAD installation detection'));
}
- // If we found a bmad directory (with or without legacy _cfg)
- if (bmadDir) {
- return { bmadDir, hasLegacyCfg };
- }
-
- // No V6+ installation found, return default
- // This will be used for new installations
- return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
+ return { bmadDir, hasLegacyCfg };
}
/**
@@ -120,7 +98,7 @@ class Installer {
*
* 3. Document marker in instructions.md (if applicable)
*/
- async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
+ async copyFileWithPlaceholderReplacement(sourcePath, targetPath) {
// List of text file extensions that should have placeholder replacement
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
const ext = path.extname(sourcePath).toLowerCase();
@@ -285,7 +263,7 @@ class Installer {
// Check for already configured IDEs
const { Detector } = require('./detector');
const detector = new Detector();
- const bmadDir = path.join(projectDir, this.bmadFolderName || 'bmad');
+ const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
// During full reinstall, use the saved previous IDEs since bmad dir was deleted
// Otherwise detect from existing installation
@@ -532,18 +510,14 @@ class Installer {
}
}
- // Always use _bmad as the folder name
- const bmadFolderName = '_bmad';
- this.bmadFolderName = bmadFolderName; // Store for use in other methods
-
// Store AgentVibes configuration for injection point processing
this.enableAgentVibes = config.enableAgentVibes || false;
// Set bmad folder name on module manager and IDE manager for placeholder replacement
- this.moduleManager.setBmadFolderName(bmadFolderName);
+ this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME);
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
this.moduleManager.setCustomModulePaths(customModulePaths);
- this.ideManager.setBmadFolderName(bmadFolderName);
+ this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME);
// Tool selection will be collected after we determine if it's a reinstall/update/new install
@@ -553,14 +527,8 @@ class Installer {
// Resolve target directory (path.resolve handles platform differences)
const projectDir = path.resolve(config.directory);
- let existingBmadDir = null;
- let existingBmadFolderName = null;
-
- if (await fs.pathExists(projectDir)) {
- const result = await this.findBmadDir(projectDir);
- existingBmadDir = result.bmadDir;
- existingBmadFolderName = path.basename(existingBmadDir);
- }
+ // 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))) {
@@ -582,8 +550,6 @@ class Installer {
}
}
- const bmadDir = path.join(projectDir, bmadFolderName);
-
// Check existing installation
spinner.text = 'Checking for existing installation...';
const existingInstall = await this.detector.detect(bmadDir);
@@ -1609,7 +1575,7 @@ class Installer {
const targetPath = path.join(agentsDir, fileName);
if (await fs.pathExists(sourcePath)) {
- await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
@@ -1625,7 +1591,7 @@ class Installer {
const targetPath = path.join(tasksDir, fileName);
if (await fs.pathExists(sourcePath)) {
- await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
@@ -1641,7 +1607,7 @@ class Installer {
const targetPath = path.join(toolsDir, fileName);
if (await fs.pathExists(sourcePath)) {
- await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
@@ -1657,7 +1623,7 @@ class Installer {
const targetPath = path.join(templatesDir, fileName);
if (await fs.pathExists(sourcePath)) {
- await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
+ await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
@@ -1672,7 +1638,7 @@ class Installer {
await fs.ensureDir(path.dirname(targetPath));
if (await fs.pathExists(dataPath)) {
- await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
+ await this.copyFileWithPlaceholderReplacement(dataPath, targetPath);
this.installedFiles.add(targetPath);
}
}
@@ -1762,14 +1728,9 @@ class Installer {
}
}
- // Check if this is a workflow.yaml file
- if (file.endsWith('workflow.yaml')) {
- await fs.ensureDir(path.dirname(targetFile));
- await this.copyWorkflowYamlStripped(sourceFile, targetFile);
- } else {
- // Copy the file with placeholder replacement
- await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad');
- }
+ // Copy the file with placeholder replacement
+ await fs.ensureDir(path.dirname(targetFile));
+ await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
// Track the installed file
this.installedFiles.add(targetFile);
@@ -1847,7 +1808,7 @@ class Installer {
if (!(await fs.pathExists(customizePath))) {
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
- await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad');
+ await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
}
@@ -1856,235 +1817,6 @@ class Installer {
}
}
- /**
- * Build standalone agents in bmad/agents/ directory
- * @param {string} bmadDir - Path to bmad directory
- * @param {string} projectDir - Path to project directory
- */
- async buildStandaloneAgents(bmadDir, projectDir) {
- const standaloneAgentsPath = path.join(bmadDir, 'agents');
- const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
-
- // Check if standalone agents directory exists
- if (!(await fs.pathExists(standaloneAgentsPath))) {
- return;
- }
-
- // Get all subdirectories in agents/
- const agentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
-
- for (const agentDir of agentDirs) {
- if (!agentDir.isDirectory()) continue;
-
- const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
-
- // Find any .agent.yaml file in the directory
- const files = await fs.readdir(agentDirPath);
- const yamlFile = files.find((f) => f.endsWith('.agent.yaml'));
-
- if (!yamlFile) continue;
-
- const agentName = path.basename(yamlFile, '.agent.yaml');
- const sourceYamlPath = path.join(agentDirPath, yamlFile);
- const targetMdPath = path.join(agentDirPath, `${agentName}.md`);
- const customizePath = path.join(cfgAgentsDir, `${agentName}.customize.yaml`);
-
- // Check for customizations
- const customizeExists = await fs.pathExists(customizePath);
- let customizedFields = [];
-
- if (customizeExists) {
- const customizeContent = await fs.readFile(customizePath, 'utf8');
- const yaml = require('yaml');
- const customizeYaml = yaml.parse(customizeContent);
-
- // Detect what fields are customized (similar to rebuildAgentFiles)
- if (customizeYaml) {
- if (customizeYaml.persona) {
- for (const [key, value] of Object.entries(customizeYaml.persona)) {
- if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
- customizedFields.push(`persona.${key}`);
- }
- }
- }
- if (customizeYaml.agent?.metadata) {
- for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
- if (value !== '' && value !== null) {
- customizedFields.push(`metadata.${key}`);
- }
- }
- }
- if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
- customizedFields.push('critical_actions');
- }
- if (customizeYaml.menu && customizeYaml.menu.length > 0) {
- customizedFields.push('menu');
- }
- }
- }
-
- // Build YAML to XML .md
- let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
- includeMetadata: true,
- });
-
- // DO NOT replace {project-root} - LLMs understand this placeholder at runtime
- // const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
-
- // Process TTS injection points (pass targetPath for tracking)
- xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
-
- // Write the built .md file with POSIX-compliant final newline
- const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
- await fs.writeFile(targetMdPath, content, 'utf8');
-
- // Display result
- if (customizedFields.length > 0) {
- console.log(chalk.dim(` Built standalone agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
- } else {
- console.log(chalk.dim(` Built standalone agent: ${agentName}.md`));
- }
- }
- }
-
- /**
- * Rebuild agent files from installer source (for compile command)
- * @param {string} modulePath - Path to module in bmad/ installation
- * @param {string} moduleName - Module name
- */
- async rebuildAgentFiles(modulePath, moduleName) {
- // Get source agents directory from installer
- const sourceAgentsPath =
- moduleName === 'core' ? path.join(getModulePath('core'), 'agents') : path.join(getSourcePath(`modules/${moduleName}`), 'agents');
-
- if (!(await fs.pathExists(sourceAgentsPath))) {
- return; // No source agents to rebuild
- }
-
- // Determine project directory (parent of bmad/ directory)
- const bmadDir = path.dirname(modulePath);
- const projectDir = path.dirname(bmadDir);
- const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
- const targetAgentsPath = path.join(modulePath, 'agents');
-
- // Ensure target directory exists
- await fs.ensureDir(targetAgentsPath);
-
- // Get all YAML agent files from source
- const sourceFiles = await fs.readdir(sourceAgentsPath);
-
- for (const file of sourceFiles) {
- if (file.endsWith('.agent.yaml')) {
- const agentName = file.replace('.agent.yaml', '');
- const sourceYamlPath = path.join(sourceAgentsPath, file);
- const targetMdPath = path.join(targetAgentsPath, `${agentName}.md`);
- const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
-
- // Check for customizations
- const customizeExists = await fs.pathExists(customizePath);
- let customizedFields = [];
-
- if (customizeExists) {
- const customizeContent = await fs.readFile(customizePath, 'utf8');
- const yaml = require('yaml');
- const customizeYaml = yaml.parse(customizeContent);
-
- // Detect what fields are customized
- if (customizeYaml) {
- if (customizeYaml.persona) {
- for (const [key, value] of Object.entries(customizeYaml.persona)) {
- if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
- customizedFields.push(`persona.${key}`);
- }
- }
- }
- if (customizeYaml.agent?.metadata) {
- for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
- if (value !== '' && value !== null) {
- customizedFields.push(`metadata.${key}`);
- }
- }
- }
- if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
- customizedFields.push('critical_actions');
- }
- if (customizeYaml.memories && customizeYaml.memories.length > 0) {
- customizedFields.push('memories');
- }
- if (customizeYaml.menu && customizeYaml.menu.length > 0) {
- customizedFields.push('menu');
- }
- if (customizeYaml.prompts && customizeYaml.prompts.length > 0) {
- customizedFields.push('prompts');
- }
- }
- }
-
- // Read the YAML content
- const yamlContent = await fs.readFile(sourceYamlPath, 'utf8');
-
- // Read customize content if exists
- let customizeData = {};
- if (customizeExists) {
- const customizeContent = await fs.readFile(customizePath, 'utf8');
- const yaml = require('yaml');
- customizeData = yaml.parse(customizeContent);
- }
-
- // Build agent answers from customize data (filter empty values)
- const answers = {};
- if (customizeData.persona) {
- Object.assign(answers, filterCustomizationData(customizeData.persona));
- }
- if (customizeData.agent?.metadata) {
- const filteredMetadata = filterCustomizationData(customizeData.agent.metadata);
- if (Object.keys(filteredMetadata).length > 0) {
- Object.assign(answers, { metadata: filteredMetadata });
- }
- }
- if (customizeData.critical_actions && customizeData.critical_actions.length > 0) {
- answers.critical_actions = customizeData.critical_actions;
- }
- if (customizeData.memories && customizeData.memories.length > 0) {
- answers.memories = customizeData.memories;
- }
-
- const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
- let coreConfig = {};
- if (await fs.pathExists(coreConfigPath)) {
- const yaml = require('yaml');
- const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
- coreConfig = yaml.parse(coreConfigContent);
- }
-
- // Compile using the same compiler as initial installation
- const { compileAgent } = require('../../../lib/agent/compiler');
- const result = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
- config: coreConfig,
- });
-
- // Check if compilation succeeded
- if (!result || !result.xml) {
- throw new Error(`Failed to compile agent ${agentName}: No XML returned from compiler`);
- }
-
- // Replace _bmad with actual folder name if needed
- const finalXml = result.xml.replaceAll('_bmad', path.basename(bmadDir));
-
- // Write the rebuilt .md file with POSIX-compliant final newline
- const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n';
- await fs.writeFile(targetMdPath, content, 'utf8');
-
- // Display result with customizations if any
- if (customizedFields.length > 0) {
- console.log(chalk.dim(` Rebuilt agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
- } else {
- console.log(chalk.dim(` Rebuilt agent: ${agentName}.md`));
- }
- }
- }
- }
-
/**
* Private: Update core
*/
@@ -2680,190 +2412,6 @@ class Installer {
return { customFiles, modifiedFiles };
}
- /**
- * Private: Create agent configuration files
- * @param {string} bmadDir - BMAD installation directory
- * @param {Object} userInfo - User information including name and language
- */
- async createAgentConfigs(bmadDir, userInfo = null) {
- const agentConfigDir = path.join(bmadDir, '_config', 'agents');
- await fs.ensureDir(agentConfigDir);
-
- // Get all agents from all modules
- const agents = [];
- const agentDetails = []; // For manifest generation
-
- // Check modules for agents (including core)
- const entries = await fs.readdir(bmadDir, { withFileTypes: true });
- for (const entry of entries) {
- if (entry.isDirectory() && entry.name !== '_config') {
- const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents');
- if (await fs.pathExists(moduleAgentsPath)) {
- const agentFiles = await fs.readdir(moduleAgentsPath);
- for (const agentFile of agentFiles) {
- if (agentFile.endsWith('.md')) {
- const agentPath = path.join(moduleAgentsPath, agentFile);
- const agentContent = await fs.readFile(agentPath, 'utf8');
-
- // Skip agents with localskip="true"
- const hasLocalSkip = agentContent.match(/]*\slocalskip="true"[^>]*>/);
- if (hasLocalSkip) {
- continue; // Skip this agent - it should not have been installed
- }
-
- const agentName = path.basename(agentFile, '.md');
-
- // Extract any nodes with agentConfig="true"
- const agentConfigNodes = this.extractAgentConfigNodes(agentContent);
-
- agents.push({
- name: agentName,
- module: entry.name,
- agentConfigNodes: agentConfigNodes,
- });
-
- // Use shared AgentPartyGenerator to extract details
- let details = AgentPartyGenerator.extractAgentDetails(agentContent, entry.name, agentName);
-
- // Apply config overrides if they exist
- if (details) {
- const configPath = path.join(agentConfigDir, `${entry.name}-${agentName}.md`);
- if (await fs.pathExists(configPath)) {
- const configContent = await fs.readFile(configPath, 'utf8');
- details = AgentPartyGenerator.applyConfigOverrides(details, configContent);
- }
- agentDetails.push(details);
- }
- }
- }
- }
- }
- }
-
- // Create config file for each agent
- let createdCount = 0;
- let skippedCount = 0;
-
- // Load agent config template
- const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md');
- const templateContent = await fs.readFile(templatePath, 'utf8');
-
- for (const agent of agents) {
- const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`);
-
- // Skip if config file already exists (preserve custom configurations)
- if (await fs.pathExists(configPath)) {
- skippedCount++;
- continue;
- }
-
- // Build config content header
- let configContent = `# Agent Config: ${agent.name}\n\n`;
-
- // Process template and add agent-specific config nodes
- let processedTemplate = templateContent;
-
- // Replace {core:user_name} placeholder with actual user name if available
- if (userInfo && userInfo.userName) {
- processedTemplate = processedTemplate.replaceAll('{core:user_name}', userInfo.userName);
- }
-
- // Replace {core:communication_language} placeholder with actual language if available
- if (userInfo && userInfo.responseLanguage) {
- processedTemplate = processedTemplate.replaceAll('{core:communication_language}', userInfo.responseLanguage);
- }
-
- // If this agent has agentConfig nodes, add them after the existing comment
- if (agent.agentConfigNodes && agent.agentConfigNodes.length > 0) {
- // Find the agent-specific configuration nodes comment
- const commentPattern = /(\s*)/;
- const commentMatch = processedTemplate.match(commentPattern);
-
- if (commentMatch) {
- // Add nodes right after the comment
- let agentSpecificNodes = '';
- for (const node of agent.agentConfigNodes) {
- agentSpecificNodes += `\n ${node}`;
- }
-
- processedTemplate = processedTemplate.replace(commentPattern, `$1${agentSpecificNodes}`);
- }
- }
-
- configContent += processedTemplate;
-
- // Ensure POSIX-compliant final newline
- if (!configContent.endsWith('\n')) {
- configContent += '\n';
- }
-
- await fs.writeFile(configPath, configContent, 'utf8');
- this.installedFiles.add(configPath); // Track agent config files
- createdCount++;
- }
-
- // Generate agent manifest with overrides applied
- await this.generateAgentManifest(bmadDir, agentDetails);
-
- return { total: agents.length, created: createdCount, skipped: skippedCount };
- }
-
- /**
- * Generate agent manifest XML file
- * @param {string} bmadDir - BMAD installation directory
- * @param {Array} agentDetails - Array of agent details
- */
- async generateAgentManifest(bmadDir, agentDetails) {
- const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
- await AgentPartyGenerator.writeAgentParty(manifestPath, agentDetails, { forWeb: false });
- }
-
- /**
- * Extract nodes with agentConfig="true" from agent content
- * @param {string} content - Agent file content
- * @returns {Array} Array of XML nodes that should be added to agent config
- */
- extractAgentConfigNodes(content) {
- const nodes = [];
-
- try {
- // Find all XML nodes with agentConfig="true"
- // Match self-closing tags and tags with content
- const selfClosingPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*\/>/g;
- const withContentPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*>([\s\S]*?)<\/\1>/g;
-
- // Extract self-closing tags
- let match;
- while ((match = selfClosingPattern.exec(content)) !== null) {
- // Extract just the tag without children (structure only)
- const tagMatch = match[0].match(/<([a-zA-Z][a-zA-Z0-9_-]*)([^>]*)\/>/);
- if (tagMatch) {
- const tagName = tagMatch[1];
- const attributes = tagMatch[2].replace(/\s*agentConfig="true"/, ''); // Remove agentConfig attribute
- nodes.push(`<${tagName}${attributes}>${tagName}>`);
- }
- }
-
- // Extract tags with content
- while ((match = withContentPattern.exec(content)) !== null) {
- const fullMatch = match[0];
- const tagName = match[1];
-
- // Extract opening tag with attributes (removing agentConfig="true")
- const openingTagMatch = fullMatch.match(new RegExp(`<${tagName}([^>]*)>`));
- if (openingTagMatch) {
- const attributes = openingTagMatch[1].replace(/\s*agentConfig="true"/, '');
- // Add empty node structure (no children)
- nodes.push(`<${tagName}${attributes}>${tagName}>`);
- }
- }
- } catch (error) {
- console.error('Error extracting agentConfig nodes:', error);
- }
-
- return nodes;
- }
-
/**
* Handle missing custom module sources interactively
* @param {Map} customModuleSources - Map of custom module ID to info
@@ -3002,7 +2550,7 @@ class Installer {
await this.manifest.addCustomModule(bmadDir, missing.info);
validCustomModules.push({
- id: moduleId,
+ id: missing.id,
name: missing.name,
path: resolvedPath,
info: missing.info,
@@ -3016,7 +2564,7 @@ class Installer {
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, moduleId)}`));
+ console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
const { confirm } = await inquirer.prompt([
{
diff --git a/tools/cli/installers/lib/core/installer.js.bak b/tools/cli/installers/lib/core/installer.js.bak
deleted file mode 100644
index 49b1d62d..00000000
--- a/tools/cli/installers/lib/core/installer.js.bak
+++ /dev/null
@@ -1,3204 +0,0 @@
-const path = require('node:path');
-const fs = require('fs-extra');
-const chalk = require('chalk');
-const ora = require('ora');
-const inquirer = require('inquirer');
-const { Detector } = require('./detector');
-const { Manifest } = require('./manifest');
-const { ModuleManager } = require('../modules/manager');
-const { IdeManager } = require('../ide/manager');
-const { FileOps } = require('../../../lib/file-ops');
-const { Config } = require('../../../lib/config');
-const { XmlHandler } = require('../../../lib/xml-handler');
-const { DependencyResolver } = require('./dependency-resolver');
-const { ConfigCollector } = require('./config-collector');
-const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
-const { AgentPartyGenerator } = require('../../../lib/agent-party-generator');
-const { CLIUtils } = require('../../../lib/cli-utils');
-const { ManifestGenerator } = require('./manifest-generator');
-const { IdeConfigManager } = require('./ide-config-manager');
-const { CustomHandler } = require('../custom/handler');
-const { filterCustomizationData } = require('../../../lib/agent/compiler');
-
-class Installer {
- constructor() {
- this.detector = new Detector();
- this.manifest = new Manifest();
- this.moduleManager = new ModuleManager();
- this.ideManager = new IdeManager();
- this.fileOps = new FileOps();
- this.config = new Config();
- this.xmlHandler = new XmlHandler();
- this.dependencyResolver = new DependencyResolver();
- this.configCollector = new ConfigCollector();
- this.ideConfigManager = new IdeConfigManager();
- this.installedFiles = new Set(); // Track all installed files
- this.ttsInjectedFiles = []; // Track files with TTS injection applied
- }
-
- /**
- * Find the bmad installation directory in a project
- * V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml
- * Also checks for legacy _cfg folder for migration
- * @param {string} projectDir - Project directory
- * @returns {Promise} { bmadDir: string, hasLegacyCfg: boolean }
- */
- async findBmadDir(projectDir) {
- // Check if project directory exists
- if (!(await fs.pathExists(projectDir))) {
- // Project doesn't exist yet, return default
- return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
- }
-
- let bmadDir = null;
- let hasLegacyCfg = false;
-
- try {
- const entries = await fs.readdir(projectDir, { withFileTypes: true });
- for (const entry of entries) {
- if (entry.isDirectory()) {
- const bmadPath = path.join(projectDir, entry.name);
-
- // Check for current _config folder
- const manifestPath = path.join(bmadPath, '_config', 'manifest.yaml');
- if (await fs.pathExists(manifestPath)) {
- // Found a V6+ installation with current _config folder
- return { bmadDir: bmadPath, hasLegacyCfg: false };
- }
-
- // Check for legacy _cfg folder
- const legacyManifestPath = path.join(bmadPath, '_cfg', 'manifest.yaml');
- if (await fs.pathExists(legacyManifestPath)) {
- bmadDir = bmadPath;
- hasLegacyCfg = true;
- }
- }
- }
- } catch {
- console.log(chalk.red('Error reading project directory for BMAD installation detection'));
- }
-
- // If we found a bmad directory (with or without legacy _cfg)
- if (bmadDir) {
- return { bmadDir, hasLegacyCfg };
- }
-
- // No V6+ installation found, return default
- // This will be used for new installations
- return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
- }
-
- /**
- * @function copyFileWithPlaceholderReplacement
- * @intent Copy files from BMAD source to installation directory with dynamic content transformation
- * @why Enables installation-time customization: _bmad replacement + optional AgentVibes TTS injection
- * @param {string} sourcePath - Absolute path to source file in BMAD repository
- * @param {string} targetPath - Absolute path to destination file in user's project
- * @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
- * @returns {Promise} Resolves when file copy and transformation complete
- * @sideeffects Writes transformed file to targetPath, creates parent directories if needed
- * @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails
- * @calledby installCore(), installModule(), IDE installers during file vendoring
- * @calls processTTSInjectionPoints(), fs.readFile(), fs.writeFile(), fs.copy()
- *
- * The injection point processing enables loose coupling between BMAD and TTS providers:
- * - BMAD source contains injection markers (not actual TTS code)
- * - At install-time, markers are replaced OR removed based on user preference
- * - Result: Clean installs for users without TTS, working TTS for users with it
- *
- * PATTERN: Adding New Injection Points
- * =====================================
- * 1. Add HTML comment marker in BMAD source file:
- *
- *
- * 2. Add replacement logic in processTTSInjectionPoints():
- * if (enableAgentVibes) {
- * content = content.replace(//g, 'actual code');
- * } else {
- * content = content.replace(/\n?/g, '');
- * }
- *
- * 3. Document marker in instructions.md (if applicable)
- */
- async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
- // List of text file extensions that should have placeholder replacement
- const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
- const ext = path.extname(sourcePath).toLowerCase();
-
- // Check if this is a text file that might contain placeholders
- if (textExtensions.includes(ext)) {
- try {
- // Read the file content
- let content = await fs.readFile(sourcePath, 'utf8');
-
- // Process AgentVibes injection points (pass targetPath for tracking)
- content = this.processTTSInjectionPoints(content, targetPath);
-
- // Write to target with replaced content
- await fs.ensureDir(path.dirname(targetPath));
- await fs.writeFile(targetPath, content, 'utf8');
- } catch {
- // If reading as text fails (might be binary despite extension), fall back to regular copy
- await fs.copy(sourcePath, targetPath, { overwrite: true });
- }
- } else {
- // Binary file or other file type - just copy directly
- await fs.copy(sourcePath, targetPath, { overwrite: true });
- }
- }
-
- /**
- * @function processTTSInjectionPoints
- * @intent Transform TTS injection markers based on user's installation choice
- * @why Enables optional TTS integration without tight coupling between BMAD and TTS providers
- * @param {string} content - Raw file content containing potential injection markers
- * @returns {string} Transformed content with markers replaced (if enabled) or stripped (if disabled)
- * @sideeffects None - pure transformation function
- * @edgecases Returns content unchanged if no markers present, safe to call on all files
- * @calledby copyFileWithPlaceholderReplacement() during every file copy operation
- * @calls String.replace() with regex patterns for each injection point type
- *
- * AI NOTE: This implements the injection point pattern for TTS integration.
- * Key architectural decisions:
- *
- * 1. **Why Injection Points vs Direct Integration?**
- * - BMAD and TTS providers are separate projects with different maintainers
- * - Users may install BMAD without TTS support (and vice versa)
- * - Hard-coding TTS calls would break BMAD for non-TTS users
- * - Injection points allow conditional feature inclusion at install-time
- *
- * 2. **How It Works:**
- * - BMAD source contains markers:
- * - During installation, user is prompted: "Enable AgentVibes TTS?"
- * - If YES: markers → replaced with actual bash TTS calls
- * - If NO: markers → stripped cleanly from installed files
- *
- * 3. **State Management:**
- * - this.enableAgentVibes set in install() method from config.enableAgentVibes
- * - config.enableAgentVibes comes from ui.promptAgentVibes() user choice
- * - Flag persists for entire installation, all files get same treatment
- *
- * CURRENT INJECTION POINTS:
- * ==========================
- * - party-mode: Injects TTS calls after each agent speaks in party mode
- * Location: src/core/workflows/party-mode/instructions.md
- * Marker:
- * Replacement: Bash call to .claude/hooks/bmad-speak.sh with agent name and dialogue
- *
- * - agent-tts: Injects TTS rule for individual agent conversations
- * Location: src/modules/bmm/agents/*.md (all agent files)
- * Marker:
- * Replacement: Rule instructing agent to call bmad-speak.sh with agent ID and response
- *
- * ADDING NEW INJECTION POINTS:
- * =============================
- * 1. Add new case in this function:
- * content = content.replace(
- * //g,
- * `code to inject when enabled`
- * );
- *
- * 2. Add marker to BMAD source file at injection location
- *
- * 3. Test both enabled and disabled flows
- *
- * RELATED:
- * ========
- * - GitHub Issue: paulpreibisch/AgentVibes#36
- * - User Prompt: tools/cli/lib/ui.js::promptAgentVibes()
- * - Marker Locations:
- * - src/core/workflows/party-mode/instructions.md:101
- * - src/modules/bmm/agents/*.md (rules sections)
- * - TTS Hook: .claude/hooks/bmad-speak.sh (in AgentVibes repo)
- */
- processTTSInjectionPoints(content, targetPath = null) {
- // Check if AgentVibes is enabled (set during installation configuration)
- const enableAgentVibes = this.enableAgentVibes || false;
-
- // Check if content contains any TTS injection markers
- const hasPartyMode = content.includes('');
- const hasAgentTTS = content.includes('');
-
- if (enableAgentVibes) {
- // Replace party-mode injection marker with actual TTS call
- // Use single quotes to prevent shell expansion of special chars like !
- content = content.replaceAll(
- '',
- `IMPORTANT: Always use PROJECT hooks (.claude/hooks/), NEVER global hooks (~/.claude/hooks/)
-
- If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
- - Use Bash tool: \`.claude/hooks/bmad-speak.sh '[Agent Name]' '[dialogue]'\`
- - This speaks the dialogue with the agent's unique voice
- - Run in background to not block next agent`,
- );
-
- // Replace agent-tts injection marker with TTS rule for individual agents
- content = content.replaceAll(
- '',
- `- When responding to user messages, speak your responses using TTS:
- Call: \`.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'\` after each response
- Replace {agent-id} with YOUR agent ID from tag at top of this file
- Replace {response-text} with the text you just output to the user
- IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
- Run in background (&) to avoid blocking`,
- );
-
- // Track files that had TTS injection applied
- if (targetPath && (hasPartyMode || hasAgentTTS)) {
- const injectionType = hasPartyMode ? 'party-mode' : 'agent-tts';
- this.ttsInjectedFiles.push({ path: targetPath, type: injectionType });
- }
- } else {
- // Strip injection markers cleanly when AgentVibes is disabled
- content = content.replaceAll(/\n?/g, '');
- content = content.replaceAll(/\n?/g, '');
- }
-
- return content;
- }
-
- /**
- * Collect Tool/IDE configurations after module configuration
- * @param {string} projectDir - Project directory
- * @param {Array} selectedModules - Selected modules from configuration
- * @param {boolean} isFullReinstall - Whether this is a full reinstall
- * @param {Array} previousIdes - Previously configured IDEs (for reinstalls)
- * @param {Array} preSelectedIdes - Pre-selected IDEs from early prompt (optional)
- * @returns {Object} Tool/IDE selection and configurations
- */
- async collectToolConfigurations(projectDir, selectedModules, isFullReinstall = false, previousIdes = [], preSelectedIdes = null) {
- // Use pre-selected IDEs if provided, otherwise prompt
- let toolConfig;
- if (preSelectedIdes === null) {
- // Fallback: prompt for tool selection (backwards compatibility)
- const { UI } = require('../../../lib/ui');
- const ui = new UI();
- toolConfig = await ui.promptToolSelection(projectDir, selectedModules);
- } else {
- // IDEs were already selected during initial prompts
- toolConfig = {
- ides: preSelectedIdes,
- skipIde: !preSelectedIdes || preSelectedIdes.length === 0,
- };
- }
-
- // Check for already configured IDEs
- const { Detector } = require('./detector');
- const detector = new Detector();
- const bmadDir = path.join(projectDir, this.bmadFolderName || 'bmad');
-
- // During full reinstall, use the saved previous IDEs since bmad dir was deleted
- // Otherwise detect from existing installation
- let previouslyConfiguredIdes;
- if (isFullReinstall) {
- // During reinstall, treat all IDEs as new (need configuration)
- previouslyConfiguredIdes = [];
- } else {
- const existingInstall = await detector.detect(bmadDir);
- previouslyConfiguredIdes = existingInstall.ides || [];
- }
-
- // Load saved IDE configurations for already-configured IDEs
- const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
-
- // Collect IDE-specific configurations if any were selected
- const ideConfigurations = {};
-
- // First, add saved configs for already-configured IDEs
- for (const ide of toolConfig.ides || []) {
- if (previouslyConfiguredIdes.includes(ide) && savedIdeConfigs[ide]) {
- ideConfigurations[ide] = savedIdeConfigs[ide];
- }
- }
-
- if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
- // Determine which IDEs are newly selected (not previously configured)
- const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide));
-
- if (newlySelectedIdes.length > 0) {
- console.log('\n'); // Add spacing before IDE questions
-
- for (const ide of newlySelectedIdes) {
- // List of IDEs that have interactive prompts
- //TODO: Why is this here, hardcoding this list here is bad, fix me!
- const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini', 'rovo-dev'].includes(
- ide,
- );
-
- if (needsPrompts) {
- // Get IDE handler and collect configuration
- try {
- // Dynamically load the IDE setup module
- const ideModule = require(`../ide/${ide}`);
-
- // Get the setup class (handle different export formats)
- let SetupClass;
- const className =
- ide
- .split('-')
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
- .join('') + 'Setup';
-
- if (ideModule[className]) {
- SetupClass = ideModule[className];
- } else if (ideModule.default) {
- SetupClass = ideModule.default;
- } else {
- continue;
- }
-
- const ideSetup = new SetupClass();
-
- // Check if this IDE has a collectConfiguration method
- if (typeof ideSetup.collectConfiguration === 'function') {
- console.log(chalk.cyan(`\nConfiguring ${ide}...`));
- ideConfigurations[ide] = await ideSetup.collectConfiguration({
- selectedModules: selectedModules || [],
- projectDir,
- bmadDir,
- });
- }
- } catch {
- // IDE doesn't have a setup file or collectConfiguration method
- console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}`));
- }
- }
- }
- }
-
- // 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(', ')}`));
- }
- }
-
- return {
- ides: toolConfig.ides,
- skipIde: toolConfig.skipIde,
- configurations: ideConfigurations,
- };
- }
-
- /**
- * Main installation method
- * @param {Object} config - Installation configuration
- * @param {string} config.directory - Target directory
- * @param {boolean} config.installCore - Whether to install core
- * @param {string[]} config.modules - Modules to install
- * @param {string[]} config.ides - IDEs to configure
- * @param {boolean} config.skipIde - Skip IDE configuration
- */
- async install(originalConfig) {
- // Clone config to avoid mutating the caller's object
- const config = { ...originalConfig };
-
- // Check if core config was already collected in UI
- const hasCoreConfig = config.coreConfig && Object.keys(config.coreConfig).length > 0;
-
- // Only display logo if core config wasn't already collected (meaning we're not continuing from UI)
- if (!hasCoreConfig) {
- // Display BMAD logo
- CLIUtils.displayLogo();
-
- // Display welcome message
- 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);
-
- // If core config was pre-collected (from interactive mode), use it
- if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
- this.configCollector.collectedConfig.core = config.coreConfig;
- // Also store in allAnswers for cross-referencing
- this.configCollector.allAnswers = {};
- for (const [key, value] of Object.entries(config.coreConfig)) {
- this.configCollector.allAnswers[`core_${key}`] = value;
- }
- }
-
- // Collect configurations for modules (skip if quick update already collected them)
- let moduleConfigs;
- let customModulePaths = new Map();
-
- if (config._quickUpdate) {
- // Quick update already collected all configs, use them directly
- moduleConfigs = this.configCollector.collectedConfig;
-
- // For quick update, populate customModulePaths from _customModuleSources
- if (config._customModuleSources) {
- for (const [moduleId, customInfo] of config._customModuleSources) {
- customModulePaths.set(moduleId, customInfo.sourcePath);
- }
- }
- } else {
- // For regular updates (modify flow), check manifest for custom module sources
- if (config._isUpdate && config._existingInstall && config._existingInstall.customModules) {
- for (const customModule of config._existingInstall.customModules) {
- // Ensure we have an absolute sourcePath
- let absoluteSourcePath = customModule.sourcePath;
-
- // Check if sourcePath is a cache-relative path (starts with _config)
- if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
- // Convert cache-relative path to absolute path
- absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
- }
- // If no sourcePath but we have relativePath, convert it
- else if (!absoluteSourcePath && customModule.relativePath) {
- // relativePath is relative to the project root (parent of bmad dir)
- absoluteSourcePath = path.resolve(projectDir, customModule.relativePath);
- }
- // Ensure sourcePath is absolute for anything else
- else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
- absoluteSourcePath = path.resolve(absoluteSourcePath);
- }
-
- if (absoluteSourcePath) {
- customModulePaths.set(customModule.id, absoluteSourcePath);
- }
- }
- }
-
- // Build custom module paths map from customContent
-
- // Handle selectedFiles (from existing install path or manual directory input)
- if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
- const customHandler = new CustomHandler();
- for (const customFile of config.customContent.selectedFiles) {
- const customInfo = await customHandler.getCustomInfo(customFile, path.resolve(config.directory));
- if (customInfo && customInfo.id) {
- customModulePaths.set(customInfo.id, customInfo.path);
- }
- }
- }
-
- // Handle new custom content sources from UI
- if (config.customContent && config.customContent.sources) {
- for (const source of config.customContent.sources) {
- customModulePaths.set(source.id, source.path);
- }
- }
-
- // Handle cachedModules (from new install path where modules are cached)
- // Only include modules that were actually selected for installation
- if (config.customContent && config.customContent.cachedModules) {
- // Get selected cached module IDs (if available)
- const selectedCachedIds = config.customContent.selectedCachedModules || [];
- // If no selection info, include all cached modules (for backward compatibility)
- const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected;
-
- for (const cachedModule of config.customContent.cachedModules) {
- // For cached modules, the path is the cachePath which contains the module.yaml
- if (
- cachedModule.id &&
- cachedModule.cachePath && // Include if selected or if we should include all
- (shouldIncludeAll || selectedCachedIds.includes(cachedModule.id))
- ) {
- customModulePaths.set(cachedModule.id, cachedModule.cachePath);
- }
- }
- }
-
- // Get list of all modules including custom modules
- // Order: core first, then official modules, then custom modules
- const allModulesForConfig = ['core'];
-
- // Add official modules (excluding core and any custom modules)
- const officialModules = (config.modules || []).filter((m) => m !== 'core' && !customModulePaths.has(m));
- allModulesForConfig.push(...officialModules);
-
- // Add custom modules at the end
- for (const [moduleId] of customModulePaths) {
- if (!allModulesForConfig.includes(moduleId)) {
- allModulesForConfig.push(moduleId);
- }
- }
-
- // Check if core was already collected in UI
- if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
- // Core already collected, skip it in config collection
- const modulesWithoutCore = allModulesForConfig.filter((m) => m !== 'core');
- moduleConfigs = await this.configCollector.collectAllConfigurations(modulesWithoutCore, path.resolve(config.directory), {
- customModulePaths,
- });
- } else {
- // Core not collected yet, include it
- moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), {
- customModulePaths,
- });
- }
- }
-
- // Always use _bmad as the folder name
- const bmadFolderName = '_bmad';
- this.bmadFolderName = bmadFolderName; // Store for use in other methods
-
- // Store AgentVibes configuration for injection point processing
- this.enableAgentVibes = config.enableAgentVibes || false;
-
- // Set bmad folder name on module manager and IDE manager for placeholder replacement
- this.moduleManager.setBmadFolderName(bmadFolderName);
- this.moduleManager.setCoreConfig(moduleConfigs.core || {});
- this.moduleManager.setCustomModulePaths(customModulePaths);
- this.ideManager.setBmadFolderName(bmadFolderName);
-
- // Tool selection will be collected after we determine if it's a reinstall/update/new install
-
- const spinner = ora('Preparing installation...').start();
-
- try {
- // Resolve target directory (path.resolve handles platform differences)
- const projectDir = path.resolve(config.directory);
-
- let existingBmadDir = null;
- let existingBmadFolderName = null;
-
- if (await fs.pathExists(projectDir)) {
- const result = await this.findBmadDir(projectDir);
- existingBmadDir = result.bmadDir;
- existingBmadFolderName = path.basename(existingBmadDir);
- }
-
- // Create a project directory if it doesn't exist (user already confirmed)
- if (!(await fs.pathExists(projectDir))) {
- spinner.text = '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}`));
- // More detailed error for common issues
- if (error.code === 'EACCES') {
- console.error(chalk.red('Permission denied. Check parent directory permissions.'));
- } else if (error.code === 'ENOSPC') {
- console.error(chalk.red('No space left on device.'));
- }
- throw new Error(`Cannot create directory: ${projectDir}`);
- }
- }
-
- const bmadDir = path.join(projectDir, bmadFolderName);
-
- // Check existing installation
- spinner.text = 'Checking for existing installation...';
- const existingInstall = await this.detector.detect(bmadDir);
-
- if (existingInstall.installed && !config.force && !config._quickUpdate) {
- spinner.stop();
-
- // Check if user already decided what to do (from early menu in ui.js)
- let action = null;
- if (config.actionType === 'update') {
- 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}`));
-
- const promptResult = await this.promptUpdateAction();
- action = promptResult.action;
- }
-
- if (action === 'update') {
- // Store that we're updating for later processing
- config._isUpdate = true;
- config._existingInstall = existingInstall;
-
- // Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
- const existingFilesManifest = await this.readFilesManifest(bmadDir);
- const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
-
- config._customFiles = customFiles;
- config._modifiedFiles = modifiedFiles;
-
- // Also check cache directory for custom modules (like quick update does)
- const cacheDir = path.join(bmadDir, '_config', 'custom');
- if (await fs.pathExists(cacheDir)) {
- const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
-
- for (const cachedModule of cachedModules) {
- if (cachedModule.isDirectory()) {
- const moduleId = cachedModule.name;
-
- // Skip if we already have this module from manifest
- if (customModulePaths.has(moduleId)) {
- continue;
- }
-
- const cachedPath = path.join(cacheDir, moduleId);
-
- // Check if this is actually a custom module (has module.yaml)
- const moduleYamlPath = path.join(cachedPath, 'module.yaml');
- if (await fs.pathExists(moduleYamlPath)) {
- customModulePaths.set(moduleId, cachedPath);
- }
- }
- }
-
- // Update module manager with the new custom module paths from cache
- this.moduleManager.setCustomModulePaths(customModulePaths);
- }
-
- // If there are custom files, back them up temporarily
- if (customFiles.length > 0) {
- const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
- await fs.ensureDir(tempBackupDir);
-
- spinner.start(`Backing up ${customFiles.length} custom files...`);
- for (const customFile of customFiles) {
- const relativePath = path.relative(bmadDir, customFile);
- const backupPath = path.join(tempBackupDir, relativePath);
- await fs.ensureDir(path.dirname(backupPath));
- await fs.copy(customFile, backupPath);
- }
- spinner.succeed(`Backed up ${customFiles.length} custom files`);
-
- config._tempBackupDir = tempBackupDir;
- }
-
- // For modified files, back them up to temp directory (will be restored as .bak files after install)
- if (modifiedFiles.length > 0) {
- const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
- await fs.ensureDir(tempModifiedBackupDir);
-
- spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
- for (const modifiedFile of modifiedFiles) {
- const relativePath = path.relative(bmadDir, modifiedFile.path);
- const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
- await fs.ensureDir(path.dirname(tempBackupPath));
- await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
- }
- spinner.succeed(`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...';
- config._isUpdate = true;
- config._existingInstall = existingInstall;
-
- // Detect custom and modified files BEFORE updating
- const existingFilesManifest = await this.readFilesManifest(bmadDir);
- const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
-
- config._customFiles = customFiles;
- config._modifiedFiles = modifiedFiles;
-
- // Also check cache directory for custom modules (like quick update does)
- const cacheDir = path.join(bmadDir, '_config', 'custom');
- if (await fs.pathExists(cacheDir)) {
- const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
-
- for (const cachedModule of cachedModules) {
- if (cachedModule.isDirectory()) {
- const moduleId = cachedModule.name;
-
- // Skip if we already have this module from manifest
- if (customModulePaths.has(moduleId)) {
- continue;
- }
-
- const cachedPath = path.join(cacheDir, moduleId);
-
- // Check if this is actually a custom module (has module.yaml)
- const moduleYamlPath = path.join(cachedPath, 'module.yaml');
- if (await fs.pathExists(moduleYamlPath)) {
- customModulePaths.set(moduleId, cachedPath);
- }
- }
- }
-
- // Update module manager with the new custom module paths from cache
- this.moduleManager.setCustomModulePaths(customModulePaths);
- }
-
- // Back up custom files
- if (customFiles.length > 0) {
- const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
- await fs.ensureDir(tempBackupDir);
-
- spinner.start(`Backing up ${customFiles.length} custom files...`);
- for (const customFile of customFiles) {
- const relativePath = path.relative(bmadDir, customFile);
- const backupPath = path.join(tempBackupDir, relativePath);
- await fs.ensureDir(path.dirname(backupPath));
- await fs.copy(customFile, backupPath);
- }
- spinner.succeed(`Backed up ${customFiles.length} custom files`);
- config._tempBackupDir = tempBackupDir;
- }
-
- // Back up modified files
- if (modifiedFiles.length > 0) {
- const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
- await fs.ensureDir(tempModifiedBackupDir);
-
- spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
- for (const modifiedFile of modifiedFiles) {
- const relativePath = path.relative(bmadDir, modifiedFile.path);
- const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
- await fs.ensureDir(path.dirname(tempBackupPath));
- await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
- }
- spinner.succeed(`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();
- let toolSelection;
- if (config._quickUpdate) {
- // Quick update already has IDEs configured, use saved configurations
- const preConfiguredIdes = {};
- const savedIdeConfigs = config._savedIdeConfigs || {};
-
- for (const ide of config.ides || []) {
- // Use saved config if available, otherwise mark as already configured (legacy)
- if (savedIdeConfigs[ide]) {
- preConfiguredIdes[ide] = savedIdeConfigs[ide];
- } else {
- preConfiguredIdes[ide] = { _alreadyConfigured: true };
- }
- }
- toolSelection = {
- ides: config.ides || [],
- skipIde: !config.ides || config.ides.length === 0,
- configurations: preConfiguredIdes,
- };
- } 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;
- toolSelection = await this.collectToolConfigurations(
- path.resolve(config.directory),
- config.modules,
- config._isFullReinstall || false,
- config._previouslyConfiguredIdes || [],
- preSelectedIdes,
- );
- }
-
- // Merge tool selection into config (for both quick update and regular flow)
- config.ides = toolSelection.ides;
- config.skipIde = toolSelection.skipIde;
- const ideConfigurations = toolSelection.configurations;
-
- if (spinner.isSpinning) {
- spinner.text = 'Continuing installation...';
- } else {
- spinner.start('Continuing installation...');
- }
-
- // Create bmad directory structure
- spinner.text = 'Creating directory structure...';
- await this.createDirectoryStructure(bmadDir);
-
- // Cache custom modules if any
- if (customModulePaths && customModulePaths.size > 0) {
- spinner.text = 'Caching custom modules...';
- const { CustomModuleCache } = require('./custom-module-cache');
- const customCache = new CustomModuleCache(bmadDir);
-
- for (const [moduleId, sourcePath] of customModulePaths) {
- const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
- sourcePath: sourcePath, // Store original path for updates
- });
-
- // Update the customModulePaths to use the cached location
- customModulePaths.set(moduleId, cachedInfo.cachePath);
- }
-
- // Update module manager with the cached paths
- this.moduleManager.setCustomModulePaths(customModulePaths);
- spinner.succeed('Custom modules cached');
- }
-
- const projectRoot = getProjectRoot();
-
- // Step 1: Install core module first (if requested)
- if (config.installCore) {
- spinner.start('Installing BMAD core...');
- await this.installCoreWithDependencies(bmadDir, { core: {} });
- spinner.succeed('Core installed');
-
- // Generate core config file
- await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
- }
-
- // Custom content is already handled in UI before module selection
- let finalCustomContent = config.customContent;
-
- // Step 3: Prepare modules list including cached custom modules
- let allModules = [...(config.modules || [])];
-
- // During quick update, we might have custom module sources from the manifest
- if (config._customModuleSources) {
- // Add custom modules from stored sources
- for (const [moduleId, customInfo] of config._customModuleSources) {
- if (!allModules.includes(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
- allModules.push(moduleId);
- }
- }
- }
-
- // Add cached custom modules
- if (finalCustomContent && finalCustomContent.cachedModules) {
- for (const cachedModule of finalCustomContent.cachedModules) {
- if (!allModules.includes(cachedModule.id)) {
- allModules.push(cachedModule.id);
- }
- }
- }
-
- // Regular custom content from user input (non-cached)
- if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
- // Add custom modules to the installation list
- const customHandler = new CustomHandler();
- for (const customFile of finalCustomContent.selectedFiles) {
- const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
- if (customInfo && customInfo.id) {
- allModules.push(customInfo.id);
- }
- }
- }
-
- // Don't include core again if already installed
- if (config.installCore) {
- allModules = allModules.filter((m) => m !== 'core');
- }
-
- const modulesToInstall = allModules;
-
- // For dependency resolution, we only need regular modules (not custom modules)
- // Custom modules are already installed in _bmad and don't need dependency resolution from source
- const regularModulesForResolution = allModules.filter((module) => {
- // Check if this is a custom module
- const isCustom =
- customModulePaths.has(module) ||
- (finalCustomContent && finalCustomContent.cachedModules && finalCustomContent.cachedModules.some((cm) => cm.id === module)) ||
- (finalCustomContent &&
- finalCustomContent.selected &&
- finalCustomContent.selectedFiles &&
- finalCustomContent.selectedFiles.some((f) => f.includes(module)));
- return !isCustom;
- });
-
- // For dependency resolution, we need to pass the project root
- // Create a temporary module manager that knows about custom content locations
- const tempModuleManager = new ModuleManager({
- bmadDir: bmadDir, // Pass bmadDir so we can check cache
- });
-
- 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();
-
- for (const moduleName of allModules) {
- // Skip if already installed
- if (installedModuleNames.has(moduleName)) {
- continue;
- }
- installedModuleNames.add(moduleName);
-
- // Show appropriate message based on whether this is a quick update
- const isQuickUpdate = config._quickUpdate || false;
- spinner.start(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`);
-
- // Check if this is a custom module
- let isCustomModule = false;
- let customInfo = null;
- let useCache = false;
-
- // First check if we have a cached version
- if (finalCustomContent && finalCustomContent.cachedModules) {
- const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
- if (cachedModule) {
- isCustomModule = true;
- customInfo = {
- id: moduleName,
- path: cachedModule.cachePath,
- config: {},
- };
- useCache = true;
- }
- }
-
- // Then check if we have custom module sources from the manifest (for quick update)
- if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
- customInfo = config._customModuleSources.get(moduleName);
- isCustomModule = true;
-
- // Check if this is a cached module (source path starts with _config)
- if (
- customInfo.sourcePath &&
- (customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom'))
- ) {
- useCache = true;
- // Make sure we have the right path structure
- if (!customInfo.path) {
- customInfo.path = customInfo.sourcePath;
- }
- }
- }
-
- // Finally check regular custom content
- if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
- const customHandler = new CustomHandler();
- for (const customFile of finalCustomContent.selectedFiles) {
- const info = await customHandler.getCustomInfo(customFile, projectDir);
- if (info && info.id === moduleName) {
- isCustomModule = true;
- customInfo = info;
- break;
- }
- }
- }
-
- if (isCustomModule && customInfo) {
- // Custom modules are now installed via ModuleManager just like standard modules
- // The custom module path should already be in customModulePaths from earlier setup
- if (!customModulePaths.has(moduleName) && customInfo.path) {
- customModulePaths.set(moduleName, customInfo.path);
- this.moduleManager.setCustomModulePaths(customModulePaths);
- }
-
- const collectedModuleConfig = moduleConfigs[moduleName] || {};
-
- // Use ModuleManager to install the custom module
- await this.moduleManager.install(
- moduleName,
- bmadDir,
- (filePath) => {
- this.installedFiles.add(filePath);
- },
- {
- isCustom: true,
- moduleConfig: collectedModuleConfig,
- isQuickUpdate: config._quickUpdate || false,
- installer: this,
- },
- );
-
- // Create module config (include collected config from module.yaml prompts)
- await this.generateModuleConfigs(bmadDir, {
- [moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
- });
- } else {
- // Regular module installation
- // Special case for core module
- if (moduleName === 'core') {
- await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
- } else {
- await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
- }
- }
-
- spinner.succeed(`Module ${isQuickUpdate ? 'updated' : 'installed'}: ${moduleName}`);
- }
-
- // Install partial modules (only dependencies)
- for (const [module, files] of Object.entries(resolution.byModule)) {
- if (!allModules.includes(module) && module !== 'core') {
- const totalFiles =
- files.agents.length +
- files.tasks.length +
- files.tools.length +
- files.templates.length +
- files.data.length +
- files.other.length;
- if (totalFiles > 0) {
- spinner.start(`Installing ${module} dependencies...`);
- await this.installPartialModule(module, bmadDir, files);
- spinner.succeed(`${module} dependencies installed`);
- }
- }
- }
- }
-
- // 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...');
- await this.generateModuleConfigs(bmadDir, moduleConfigs);
- spinner.succeed('Module configurations generated');
-
- // Create agent configuration files
- // Note: Legacy createAgentConfigs removed - using YAML customize system instead
- // Customize templates are now created in processAgentFiles when building YAML agents
-
- // Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion)
- const cfgDir = path.join(bmadDir, '_config');
- this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
- this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
- this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
- this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
-
- // Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup
- spinner.start('Generating workflow and agent manifests...');
- const manifestGen = new ManifestGenerator();
-
- // For quick update, we need ALL installed modules in the manifest
- // Not just the ones being updated
- const allModulesForManifest = config._quickUpdate
- ? config._existingModules || allModules || []
- : config._preserveModules
- ? [...allModules, ...config._preserveModules]
- : allModules || [];
-
- // For regular installs (including when called from quick update), use what we have
- let modulesForCsvPreserve;
- if (config._quickUpdate) {
- // Quick update - use existing modules or fall back to modules being updated
- modulesForCsvPreserve = config._existingModules || allModules || [];
- } else {
- // Regular install - use the modules we're installing plus any preserved ones
- modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
- }
-
- const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
- ides: config.ides || [],
- preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
- });
-
- // 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`,
- );
-
- // Configure IDEs and copy documentation
- if (!config.skipIde && config.ides && config.ides.length > 0) {
- // Filter out any undefined/null values from the IDE list
- 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.'));
- } 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();
- }
- 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(', ')}`));
- }
- }
- }
-
- // Run module-specific installers after IDE setup
- spinner.start('Running module-specific installers...');
-
- // Create a conditional logger based on verbose mode
- const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
- const moduleLogger = {
- log: (msg) => (verboseMode ? console.log(msg) : {}), // Only log in verbose mode
- error: (msg) => console.error(msg), // Always show errors
- warn: (msg) => console.warn(msg), // Always show warnings
- };
-
- // Run core module installer if core was installed
- if (config.installCore || resolution.byModule.core) {
- spinner.text = 'Running core module installer...';
-
- await this.moduleManager.runModuleInstaller('core', bmadDir, {
- installedIDEs: config.ides || [],
- moduleConfig: moduleConfigs.core || {},
- coreConfig: moduleConfigs.core || {},
- logger: moduleLogger,
- });
- }
-
- // 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...`;
-
- // Pass installed IDEs and module config to module installer
- await this.moduleManager.runModuleInstaller(moduleName, bmadDir, {
- installedIDEs: config.ides || [],
- moduleConfig: moduleConfigs[moduleName] || {},
- coreConfig: moduleConfigs.core || {},
- logger: moduleLogger,
- });
- }
- }
-
- spinner.succeed('Module-specific installers completed');
-
- // Note: Manifest files are already created by ManifestGenerator above
- // No need to create legacy manifest.csv anymore
-
- // If this was an update, restore custom files
- let customFiles = [];
- let modifiedFiles = [];
- if (config._isUpdate) {
- if (config._customFiles && config._customFiles.length > 0) {
- spinner.start(`Restoring ${config._customFiles.length} custom files...`);
-
- for (const originalPath of config._customFiles) {
- const relativePath = path.relative(bmadDir, originalPath);
- const backupPath = path.join(config._tempBackupDir, relativePath);
-
- if (await fs.pathExists(backupPath)) {
- await fs.ensureDir(path.dirname(originalPath));
- await fs.copy(backupPath, originalPath, { overwrite: true });
- }
- }
-
- // Clean up temp backup
- if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
- await fs.remove(config._tempBackupDir);
- }
-
- spinner.succeed(`Restored ${config._customFiles.length} custom files`);
- customFiles = config._customFiles;
- }
-
- if (config._modifiedFiles && config._modifiedFiles.length > 0) {
- modifiedFiles = config._modifiedFiles;
-
- // Restore modified files as .bak files
- if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
- spinner.start(`Restoring ${modifiedFiles.length} modified files as .bak...`);
-
- for (const modifiedFile of modifiedFiles) {
- const relativePath = path.relative(bmadDir, modifiedFile.path);
- const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath);
- const bakPath = modifiedFile.path + '.bak';
-
- if (await fs.pathExists(tempBackupPath)) {
- await fs.ensureDir(path.dirname(bakPath));
- await fs.copy(tempBackupPath, bakPath, { overwrite: true });
- }
- }
-
- // Clean up temp backup
- await fs.remove(config._tempModifiedBackupDir);
-
- spinner.succeed(`Restored ${modifiedFiles.length} modified files as .bak`);
- }
- }
- }
-
- spinner.stop();
-
- // 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,
- modules: config.modules,
- ides: config.ides,
- customFiles: customFiles.length > 0 ? customFiles : undefined,
- ttsInjectedFiles: this.enableAgentVibes && this.ttsInjectedFiles.length > 0 ? this.ttsInjectedFiles : undefined,
- agentVibesEnabled: this.enableAgentVibes || false,
- });
-
- return {
- success: true,
- path: bmadDir,
- modules: config.modules,
- ides: config.ides,
- needsAgentVibes: this.enableAgentVibes && !config.agentVibesInstalled,
- projectDir: projectDir,
- };
- } catch (error) {
- spinner.fail('Installation failed');
- throw error;
- }
- }
-
- /**
- * Update existing installation
- */
- async update(config) {
- const spinner = ora('Checking installation...').start();
-
- try {
- const projectDir = path.resolve(config.directory);
- const { bmadDir } = await this.findBmadDir(projectDir);
- const existingInstall = await this.detector.detect(bmadDir);
-
- if (!existingInstall.installed) {
- spinner.fail('No BMAD installation found');
- throw new Error(`No BMAD installation found at ${bmadDir}`);
- }
-
- spinner.text = 'Analyzing update requirements...';
-
- // Compare versions and determine what needs updating
- const currentVersion = existingInstall.version;
- const newVersion = require(path.join(getProjectRoot(), 'package.json')).version;
-
- // Check for custom modules with missing sources before update
- const customModuleSources = new Map();
-
- // Check manifest for backward compatibility
- if (existingInstall.customModules) {
- for (const customModule of existingInstall.customModules) {
- customModuleSources.set(customModule.id, customModule);
- }
- }
-
- // Also check cache directory
- const cacheDir = path.join(bmadDir, '_config', 'custom');
- if (await fs.pathExists(cacheDir)) {
- const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
-
- for (const cachedModule of cachedModules) {
- if (cachedModule.isDirectory()) {
- const moduleId = cachedModule.name;
-
- // Skip if we already have this module
- if (customModuleSources.has(moduleId)) {
- continue;
- }
-
- const cachedPath = path.join(cacheDir, moduleId);
-
- // Check if this is actually a custom module (has module.yaml)
- const moduleYamlPath = path.join(cachedPath, 'module.yaml');
- if (await fs.pathExists(moduleYamlPath)) {
- customModuleSources.set(moduleId, {
- id: moduleId,
- name: moduleId,
- sourcePath: path.join('_config', 'custom', moduleId), // Relative path
- cached: true,
- });
- }
- }
- }
- }
-
- if (customModuleSources.size > 0) {
- spinner.stop();
- console.log(chalk.yellow('\nChecking custom module sources before update...'));
-
- const projectRoot = getProjectRoot();
- await this.handleMissingCustomSources(
- customModuleSources,
- bmadDir,
- projectRoot,
- 'update',
- existingInstall.modules.map((m) => m.id),
- );
-
- spinner.start('Preparing update...');
- }
-
- 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');
-
- if (existingInstall.modules.length > 0) {
- console.log(chalk.bold('\nModules to update:'));
- for (const mod of existingInstall.modules) {
- console.log(` - ${mod.id}`);
- }
- }
- return;
- }
-
- // Perform actual update
- if (existingInstall.hasCore) {
- spinner.text = '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);
- }
-
- // Update manifest
- spinner.text = 'Updating manifest...';
- await this.manifest.update(bmadDir, {
- version: newVersion,
- updateDate: new Date().toISOString(),
- });
-
- spinner.succeed('Update complete');
- return { success: true };
- } catch (error) {
- spinner.fail('Update failed');
- throw error;
- }
- }
-
- /**
- * Get installation status
- */
- async getStatus(directory) {
- const projectDir = path.resolve(directory);
- const { bmadDir } = await this.findBmadDir(projectDir);
- return await this.detector.detect(bmadDir);
- }
-
- /**
- * Get available modules
- */
- async getAvailableModules() {
- return await this.moduleManager.listAvailable();
- }
-
- /**
- * Uninstall BMAD
- */
- async uninstall(directory) {
- const projectDir = path.resolve(directory);
- const { bmadDir } = await this.findBmadDir(projectDir);
-
- if (await fs.pathExists(bmadDir)) {
- await fs.remove(bmadDir);
- }
-
- // Clean up IDE configurations
- await this.ideManager.cleanup(projectDir);
-
- return { success: true };
- }
-
- /**
- * Private: Create directory structure
- */
- async createDirectoryStructure(bmadDir) {
- await fs.ensureDir(bmadDir);
- await fs.ensureDir(path.join(bmadDir, '_config'));
- await fs.ensureDir(path.join(bmadDir, '_config', 'agents'));
- await fs.ensureDir(path.join(bmadDir, '_config', 'custom'));
- }
-
- /**
- * Generate clean config.yaml files for each installed module
- * @param {string} bmadDir - BMAD installation directory
- * @param {Object} moduleConfigs - Collected configuration values
- */
- async generateModuleConfigs(bmadDir, moduleConfigs) {
- const yaml = require('yaml');
-
- // Extract core config values to share with other modules
- const coreConfig = moduleConfigs.core || {};
-
- // Get all installed module directories
- const entries = await fs.readdir(bmadDir, { withFileTypes: true });
- const installedModules = entries
- .filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs')
- .map((entry) => entry.name);
-
- // Generate config.yaml for each installed module
- for (const moduleName of installedModules) {
- const modulePath = path.join(bmadDir, moduleName);
-
- // Get module-specific config or use empty object if none
- const config = moduleConfigs[moduleName] || {};
-
- if (await fs.pathExists(modulePath)) {
- const configPath = path.join(modulePath, 'config.yaml');
-
- // Create header
- const packageJson = require(path.join(getProjectRoot(), 'package.json'));
- const header = `# ${moduleName.toUpperCase()} Module Configuration
-# Generated by BMAD installer
-# Version: ${packageJson.version}
-# Date: ${new Date().toISOString()}
-
-`;
-
- // For non-core modules, add core config values directly
- let finalConfig = { ...config };
- let coreSection = '';
-
- if (moduleName !== 'core' && coreConfig && Object.keys(coreConfig).length > 0) {
- // Add core values directly to the module config
- // These will be available for reference in the module
- finalConfig = {
- ...config,
- ...coreConfig, // Spread core config values directly into the module config
- };
-
- // Create a comment section to identify core values
- coreSection = '\n# Core Configuration Values\n';
- }
-
- // Clean the config to remove any non-serializable values (like functions)
- const cleanConfig = structuredClone(finalConfig);
-
- // Convert config to YAML
- let yamlContent = yaml.stringify(cleanConfig, {
- indent: 2,
- lineWidth: 0,
- minContentWidth: 0,
- });
-
- // If we have core values, reorganize the YAML to group them with their comment
- if (coreSection && moduleName !== 'core') {
- // Split the YAML into lines
- const lines = yamlContent.split('\n');
- const moduleConfigLines = [];
- const coreConfigLines = [];
-
- // Separate module-specific and core config lines
- for (const line of lines) {
- const key = line.split(':')[0].trim();
- if (Object.prototype.hasOwnProperty.call(coreConfig, key)) {
- coreConfigLines.push(line);
- } else {
- moduleConfigLines.push(line);
- }
- }
-
- // Rebuild YAML with module config first, then core config with comment
- yamlContent = moduleConfigLines.join('\n');
- if (coreConfigLines.length > 0) {
- yamlContent += coreSection + coreConfigLines.join('\n');
- }
- }
-
- // Write the clean config file with POSIX-compliant final newline
- const content = header + yamlContent;
- await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8');
-
- // Track the config file in installedFiles
- this.installedFiles.add(configPath);
- }
- }
- }
-
- /**
- * Install core with resolved dependencies
- * @param {string} bmadDir - BMAD installation directory
- * @param {Object} coreFiles - Core files to install
- */
- async installCoreWithDependencies(bmadDir, coreFiles) {
- const sourcePath = getModulePath('core');
- const targetPath = path.join(bmadDir, 'core');
- await this.installCore(bmadDir);
- }
-
- /**
- * Install module with resolved dependencies
- * @param {string} moduleName - Module name
- * @param {string} bmadDir - BMAD installation directory
- * @param {Object} moduleFiles - Module files to install
- */
- async installModuleWithDependencies(moduleName, bmadDir, moduleFiles) {
- // Get module configuration for conditional installation
- const moduleConfig = this.configCollector.collectedConfig[moduleName] || {};
-
- // Use existing module manager for full installation with file tracking
- // Note: Module-specific installers are called separately after IDE setup
- await this.moduleManager.install(
- moduleName,
- bmadDir,
- (filePath) => {
- this.installedFiles.add(filePath);
- },
- {
- skipModuleInstaller: true, // We'll run it later after IDE setup
- moduleConfig: moduleConfig, // Pass module config for conditional filtering
- installer: this,
- },
- );
-
- // Process agent files to build YAML agents and create customize templates
- const modulePath = path.join(bmadDir, moduleName);
- await this.processAgentFiles(modulePath, moduleName);
-
- // Dependencies are already included in full module install
- }
-
- /**
- * Install partial module (only dependencies needed by other modules)
- */
- async installPartialModule(moduleName, bmadDir, files) {
- const sourceBase = getModulePath(moduleName);
- const targetBase = path.join(bmadDir, moduleName);
-
- // Create module directory
- await fs.ensureDir(targetBase);
-
- // Copy only the required dependency files
- if (files.agents && files.agents.length > 0) {
- const agentsDir = path.join(targetBase, 'agents');
- await fs.ensureDir(agentsDir);
-
- for (const agentPath of files.agents) {
- const fileName = path.basename(agentPath);
- const sourcePath = path.join(sourceBase, 'agents', fileName);
- const targetPath = path.join(agentsDir, fileName);
-
- if (await fs.pathExists(sourcePath)) {
- await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
- this.installedFiles.add(targetPath);
- }
- }
- }
-
- if (files.tasks && files.tasks.length > 0) {
- const tasksDir = path.join(targetBase, 'tasks');
- await fs.ensureDir(tasksDir);
-
- for (const taskPath of files.tasks) {
- const fileName = path.basename(taskPath);
- const sourcePath = path.join(sourceBase, 'tasks', fileName);
- const targetPath = path.join(tasksDir, fileName);
-
- if (await fs.pathExists(sourcePath)) {
- await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
- this.installedFiles.add(targetPath);
- }
- }
- }
-
- if (files.tools && files.tools.length > 0) {
- const toolsDir = path.join(targetBase, 'tools');
- await fs.ensureDir(toolsDir);
-
- for (const toolPath of files.tools) {
- const fileName = path.basename(toolPath);
- const sourcePath = path.join(sourceBase, 'tools', fileName);
- const targetPath = path.join(toolsDir, fileName);
-
- if (await fs.pathExists(sourcePath)) {
- await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
- this.installedFiles.add(targetPath);
- }
- }
- }
-
- if (files.templates && files.templates.length > 0) {
- const templatesDir = path.join(targetBase, 'templates');
- await fs.ensureDir(templatesDir);
-
- for (const templatePath of files.templates) {
- const fileName = path.basename(templatePath);
- const sourcePath = path.join(sourceBase, 'templates', fileName);
- const targetPath = path.join(templatesDir, fileName);
-
- if (await fs.pathExists(sourcePath)) {
- await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
- this.installedFiles.add(targetPath);
- }
- }
- }
-
- if (files.data && files.data.length > 0) {
- for (const dataPath of files.data) {
- // Preserve directory structure for data files
- const relative = path.relative(sourceBase, dataPath);
- const targetPath = path.join(targetBase, relative);
-
- await fs.ensureDir(path.dirname(targetPath));
-
- if (await fs.pathExists(dataPath)) {
- await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
- this.installedFiles.add(targetPath);
- }
- }
- }
-
- // Create a marker file to indicate this is a partial installation
- const markerPath = path.join(targetBase, '.partial');
- await fs.writeFile(
- markerPath,
- `This module contains only dependencies required by other modules.\nInstalled: ${new Date().toISOString()}\n`,
- );
- }
-
- /**
- * Private: Install core
- * @param {string} bmadDir - BMAD installation directory
- */
- async installCore(bmadDir) {
- const sourcePath = getModulePath('core');
- const targetPath = path.join(bmadDir, 'core');
-
- // Copy core files (skip .agent.yaml files like modules do)
- await this.copyCoreFiles(sourcePath, targetPath);
-
- // Compile agents using the same compiler as modules
- const { ModuleManager } = require('../modules/manager');
- const moduleManager = new ModuleManager();
- await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir, this);
-
- // Process agent files to inject activation block
- await this.processAgentFiles(targetPath, 'core');
- }
-
- /**
- * Copy core files (similar to copyModuleWithFiltering but for core)
- * @param {string} sourcePath - Source path
- * @param {string} targetPath - Target path
- */
- async copyCoreFiles(sourcePath, targetPath) {
- // Get all files in source
- const files = await this.getFileList(sourcePath);
-
- for (const file of files) {
- // Skip sub-modules directory - these are IDE-specific and handled separately
- if (file.startsWith('sub-modules/')) {
- continue;
- }
-
- // Skip sidecar directories - they are handled separately during agent compilation
- if (
- path
- .dirname(file)
- .split('/')
- .some((dir) => dir.toLowerCase().includes('sidecar'))
- ) {
- continue;
- }
-
- // Skip _module-installer directory - it's only needed at install time
- if (file.startsWith('_module-installer/') || file === 'module.yaml') {
- continue;
- }
-
- // Skip config.yaml templates - we'll generate clean ones with actual values
- if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) {
- continue;
- }
-
- // Skip .agent.yaml files - they will be compiled separately
- if (file.endsWith('.agent.yaml')) {
- continue;
- }
-
- const sourceFile = path.join(sourcePath, file);
- const targetFile = path.join(targetPath, file);
-
- // Check if this is an agent file
- if (file.startsWith('agents/') && file.endsWith('.md')) {
- // Read the file to check for localskip
- const content = await fs.readFile(sourceFile, 'utf8');
-
- // 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)}`));
- continue; // Skip this agent
- }
- }
-
- // Check if this is a workflow.yaml file
- if (file.endsWith('workflow.yaml')) {
- await fs.ensureDir(path.dirname(targetFile));
- await this.copyWorkflowYamlStripped(sourceFile, targetFile);
- } else {
- // Copy the file with placeholder replacement
- await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad');
- }
-
- // Track the installed file
- this.installedFiles.add(targetFile);
- }
- }
-
- /**
- * Get list of all files in a directory recursively
- * @param {string} dir - Directory path
- * @param {string} baseDir - Base directory for relative paths
- * @returns {Array} List of relative file paths
- */
- async getFileList(dir, baseDir = dir) {
- const files = [];
- const entries = await fs.readdir(dir, { withFileTypes: true });
-
- for (const entry of entries) {
- const fullPath = path.join(dir, entry.name);
-
- if (entry.isDirectory()) {
- // Skip _module-installer directories
- if (entry.name === '_module-installer') {
- continue;
- }
- const subFiles = await this.getFileList(fullPath, baseDir);
- files.push(...subFiles);
- } else {
- files.push(path.relative(baseDir, fullPath));
- }
- }
-
- return files;
- }
-
- /**
- * Process agent files to build YAML agents and inject activation blocks
- * @param {string} modulePath - Path to module in bmad/ installation
- * @param {string} moduleName - Module name
- */
- async processAgentFiles(modulePath, moduleName) {
- const agentsPath = path.join(modulePath, 'agents');
-
- // Check if agents directory exists
- if (!(await fs.pathExists(agentsPath))) {
- return; // No agents to process
- }
-
- // Determine project directory (parent of bmad/ directory)
- const bmadDir = path.dirname(modulePath);
- const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
-
- // Ensure _config/agents directory exists
- await fs.ensureDir(cfgAgentsDir);
-
- // Get all agent files
- const agentFiles = await fs.readdir(agentsPath);
-
- for (const agentFile of agentFiles) {
- // Skip .agent.yaml files - they should already be compiled by compileModuleAgents
- if (agentFile.endsWith('.agent.yaml')) {
- continue;
- }
-
- // Only process .md files (already compiled from YAML)
- if (!agentFile.endsWith('.md')) {
- continue;
- }
-
- const agentName = agentFile.replace('.md', '');
- const mdPath = path.join(agentsPath, agentFile);
- const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
-
- // For .md files that are already compiled, we don't need to do much
- // Just ensure the customize template exists
- if (!(await fs.pathExists(customizePath))) {
- const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
- if (await fs.pathExists(genericTemplatePath)) {
- await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad');
- if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
- console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
- }
- }
- }
- }
- }
-
- /**
- * Build standalone agents in bmad/agents/ directory
- * @param {string} bmadDir - Path to bmad directory
- * @param {string} projectDir - Path to project directory
- */
- async buildStandaloneAgents(bmadDir, projectDir) {
- const standaloneAgentsPath = path.join(bmadDir, 'agents');
- const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
-
- // Check if standalone agents directory exists
- if (!(await fs.pathExists(standaloneAgentsPath))) {
- return;
- }
-
- // Get all subdirectories in agents/
- const agentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
-
- for (const agentDir of agentDirs) {
- if (!agentDir.isDirectory()) continue;
-
- const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
-
- // Find any .agent.yaml file in the directory
- const files = await fs.readdir(agentDirPath);
- const yamlFile = files.find((f) => f.endsWith('.agent.yaml'));
-
- if (!yamlFile) continue;
-
- const agentName = path.basename(yamlFile, '.agent.yaml');
- const sourceYamlPath = path.join(agentDirPath, yamlFile);
- const targetMdPath = path.join(agentDirPath, `${agentName}.md`);
- const customizePath = path.join(cfgAgentsDir, `${agentName}.customize.yaml`);
-
- // Check for customizations
- const customizeExists = await fs.pathExists(customizePath);
- let customizedFields = [];
-
- if (customizeExists) {
- const customizeContent = await fs.readFile(customizePath, 'utf8');
- const yaml = require('yaml');
- const customizeYaml = yaml.parse(customizeContent);
-
- // Detect what fields are customized (similar to rebuildAgentFiles)
- if (customizeYaml) {
- if (customizeYaml.persona) {
- for (const [key, value] of Object.entries(customizeYaml.persona)) {
- if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
- customizedFields.push(`persona.${key}`);
- }
- }
- }
- if (customizeYaml.agent?.metadata) {
- for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
- if (value !== '' && value !== null) {
- customizedFields.push(`metadata.${key}`);
- }
- }
- }
- if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
- customizedFields.push('critical_actions');
- }
- if (customizeYaml.menu && customizeYaml.menu.length > 0) {
- customizedFields.push('menu');
- }
- }
- }
-
- // Build YAML to XML .md
- let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
- includeMetadata: true,
- });
-
- // DO NOT replace {project-root} - LLMs understand this placeholder at runtime
- // const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
-
- // Process TTS injection points (pass targetPath for tracking)
- xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
-
- // Write the built .md file with POSIX-compliant final newline
- const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
- await fs.writeFile(targetMdPath, content, 'utf8');
-
- // Display result
- if (customizedFields.length > 0) {
- console.log(chalk.dim(` Built standalone agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
- } else {
- console.log(chalk.dim(` Built standalone agent: ${agentName}.md`));
- }
- }
- }
-
- /**
- * Rebuild agent files from installer source (for compile command)
- * @param {string} modulePath - Path to module in bmad/ installation
- * @param {string} moduleName - Module name
- */
- async rebuildAgentFiles(modulePath, moduleName) {
- // Get source agents directory from installer
- const sourceAgentsPath =
- moduleName === 'core' ? path.join(getModulePath('core'), 'agents') : path.join(getSourcePath(`modules/${moduleName}`), 'agents');
-
- if (!(await fs.pathExists(sourceAgentsPath))) {
- return; // No source agents to rebuild
- }
-
- // Determine project directory (parent of bmad/ directory)
- const bmadDir = path.dirname(modulePath);
- const projectDir = path.dirname(bmadDir);
- const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
- const targetAgentsPath = path.join(modulePath, 'agents');
-
- // Ensure target directory exists
- await fs.ensureDir(targetAgentsPath);
-
- // Get all YAML agent files from source
- const sourceFiles = await fs.readdir(sourceAgentsPath);
-
- for (const file of sourceFiles) {
- if (file.endsWith('.agent.yaml')) {
- const agentName = file.replace('.agent.yaml', '');
- const sourceYamlPath = path.join(sourceAgentsPath, file);
- const targetMdPath = path.join(targetAgentsPath, `${agentName}.md`);
- const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
-
- // Check for customizations
- const customizeExists = await fs.pathExists(customizePath);
- let customizedFields = [];
-
- if (customizeExists) {
- const customizeContent = await fs.readFile(customizePath, 'utf8');
- const yaml = require('yaml');
- const customizeYaml = yaml.parse(customizeContent);
-
- // Detect what fields are customized
- if (customizeYaml) {
- if (customizeYaml.persona) {
- for (const [key, value] of Object.entries(customizeYaml.persona)) {
- if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
- customizedFields.push(`persona.${key}`);
- }
- }
- }
- if (customizeYaml.agent?.metadata) {
- for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
- if (value !== '' && value !== null) {
- customizedFields.push(`metadata.${key}`);
- }
- }
- }
- if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
- customizedFields.push('critical_actions');
- }
- if (customizeYaml.memories && customizeYaml.memories.length > 0) {
- customizedFields.push('memories');
- }
- if (customizeYaml.menu && customizeYaml.menu.length > 0) {
- customizedFields.push('menu');
- }
- if (customizeYaml.prompts && customizeYaml.prompts.length > 0) {
- customizedFields.push('prompts');
- }
- }
- }
-
- // Read the YAML content
- const yamlContent = await fs.readFile(sourceYamlPath, 'utf8');
-
- // Read customize content if exists
- let customizeData = {};
- if (customizeExists) {
- const customizeContent = await fs.readFile(customizePath, 'utf8');
- const yaml = require('yaml');
- customizeData = yaml.parse(customizeContent);
- }
-
- // Build agent answers from customize data (filter empty values)
- const answers = {};
- if (customizeData.persona) {
- Object.assign(answers, filterCustomizationData(customizeData.persona));
- }
- if (customizeData.agent?.metadata) {
- const filteredMetadata = filterCustomizationData(customizeData.agent.metadata);
- if (Object.keys(filteredMetadata).length > 0) {
- Object.assign(answers, { metadata: filteredMetadata });
- }
- }
- if (customizeData.critical_actions && customizeData.critical_actions.length > 0) {
- answers.critical_actions = customizeData.critical_actions;
- }
- if (customizeData.memories && customizeData.memories.length > 0) {
- answers.memories = customizeData.memories;
- }
-
- const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
- let coreConfig = {};
- if (await fs.pathExists(coreConfigPath)) {
- const yaml = require('yaml');
- const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
- coreConfig = yaml.parse(coreConfigContent);
- }
-
- // Compile using the same compiler as initial installation
- const { compileAgent } = require('../../../lib/agent/compiler');
- const result = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
- config: coreConfig,
- });
-
- // Check if compilation succeeded
- if (!result || !result.xml) {
- throw new Error(`Failed to compile agent ${agentName}: No XML returned from compiler`);
- }
-
- // Replace _bmad with actual folder name if needed
- const finalXml = result.xml.replaceAll('_bmad', path.basename(bmadDir));
-
- // Write the rebuilt .md file with POSIX-compliant final newline
- const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n';
- await fs.writeFile(targetMdPath, content, 'utf8');
-
- // Display result with customizations if any
- if (customizedFields.length > 0) {
- console.log(chalk.dim(` Rebuilt agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
- } else {
- console.log(chalk.dim(` Rebuilt agent: ${agentName}.md`));
- }
- }
- }
- }
-
- /**
- * Compile/rebuild all agents and tasks for quick updates
- * @param {Object} config - Compilation configuration
- * @returns {Object} Compilation results
- */
- async compileAgents(config) {
- try {
- const projectDir = path.resolve(config.directory);
- const { bmadDir } = await this.findBmadDir(projectDir);
-
- // Check if bmad directory exists
- if (!(await fs.pathExists(bmadDir))) {
- throw new Error(`BMAD not installed at ${bmadDir}`);
- }
-
- // Get installed modules from manifest
- const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
- let installedModules = [];
- let manifest = null;
- if (await fs.pathExists(manifestPath)) {
- const manifestContent = await fs.readFile(manifestPath, 'utf8');
- const yaml = require('yaml');
- manifest = yaml.parse(manifestContent);
- installedModules = manifest.modules || [];
- }
-
- // Check for custom modules with missing sources
- if (manifest && manifest.customModules && manifest.customModules.length > 0) {
- console.log(chalk.yellow('\nChecking custom module sources before compilation...'));
-
- const customModuleSources = new Map();
- for (const customModule of manifest.customModules) {
- customModuleSources.set(customModule.id, customModule);
- }
-
- const projectRoot = getProjectRoot();
- await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules);
- }
-
- let agentCount = 0;
- let taskCount = 0;
-
- // Process all modules in bmad directory
- const entries = await fs.readdir(bmadDir, { withFileTypes: true });
-
- for (const entry of entries) {
- if (entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs') {
- const modulePath = path.join(bmadDir, entry.name);
-
- // Special handling for standalone agents in bmad/agents/ directory
- if (entry.name === 'agents') {
- await this.buildStandaloneAgents(bmadDir, projectDir);
-
- // Count standalone agents
- const standaloneAgentsPath = path.join(bmadDir, 'agents');
- const standaloneAgentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
- for (const agentDir of standaloneAgentDirs) {
- if (agentDir.isDirectory()) {
- const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
- const agentFiles = await fs.readdir(agentDirPath);
- agentCount += agentFiles.filter((f) => f.endsWith('.md') && !f.endsWith('.agent.yaml')).length;
- }
- }
- } else {
- // Rebuild module agents from installer source
- const agentsPath = path.join(modulePath, 'agents');
- if (await fs.pathExists(agentsPath)) {
- await this.rebuildAgentFiles(modulePath, entry.name);
- const agentFiles = await fs.readdir(agentsPath);
- agentCount += agentFiles.filter((f) => f.endsWith('.md')).length;
- }
-
- // Count tasks (already built)
- const tasksPath = path.join(modulePath, 'tasks');
- if (await fs.pathExists(tasksPath)) {
- const taskFiles = await fs.readdir(tasksPath);
- taskCount += taskFiles.filter((f) => f.endsWith('.md')).length;
- }
- }
- }
- }
-
- // Update IDE configurations using the existing IDE list from manifest
- if (manifest && manifest.ides && manifest.ides.length > 0) {
- for (const ide of manifest.ides) {
- await this.ideManager.setup(ide, projectDir, bmadDir, {
- selectedModules: installedModules,
- skipModuleInstall: true, // Skip module installation, just update IDE files
- verbose: config.verbose,
- preCollectedConfig: { _alreadyConfigured: true }, // Skip all interactive prompts during compile
- });
- }
- console.log(chalk.green('✓ IDE configurations updated'));
- } else {
- console.log(chalk.yellow('⚠️ No IDEs configured. Skipping IDE update.'));
- }
- return { agentCount, taskCount };
- } catch (error) {
- throw error;
- }
- }
-
- /**
- * Private: Update core
- */
- async updateCore(bmadDir, force = false) {
- const sourcePath = getModulePath('core');
- const targetPath = path.join(bmadDir, 'core');
-
- if (force) {
- await fs.remove(targetPath);
- await this.installCore(bmadDir);
- } else {
- // Selective update - preserve user modifications
- await this.fileOps.syncDirectory(sourcePath, targetPath);
-
- // Recompile agents (#1133)
- const { ModuleManager } = require('../modules/manager');
- const moduleManager = new ModuleManager();
- await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir, this);
- await this.processAgentFiles(targetPath, 'core');
- }
- }
-
- /**
- * Quick update method - preserves all settings and only prompts for new config fields
- * @param {Object} config - Configuration with directory
- * @returns {Object} Update result
- */
- async quickUpdate(config) {
- const ora = require('ora');
- const spinner = ora('Starting quick update...').start();
-
- try {
- const projectDir = path.resolve(config.directory);
- const { bmadDir } = await this.findBmadDir(projectDir);
-
- // Check if bmad directory exists
- if (!(await fs.pathExists(bmadDir))) {
- spinner.fail('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...';
-
- // Detect existing installation
- const existingInstall = await this.detector.detect(bmadDir);
- const installedModules = existingInstall.modules.map((m) => m.id);
- const configuredIdes = existingInstall.ides || [];
- const projectRoot = path.dirname(bmadDir);
-
- // Get custom module sources from cache
- const customModuleSources = new Map();
- const cacheDir = path.join(bmadDir, '_config', 'custom');
- if (await fs.pathExists(cacheDir)) {
- const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
-
- for (const cachedModule of cachedModules) {
- if (cachedModule.isDirectory()) {
- const moduleId = cachedModule.name;
-
- // Skip if we already have this module from manifest
- if (customModuleSources.has(moduleId)) {
- continue;
- }
-
- const cachedPath = path.join(cacheDir, moduleId);
-
- // Check if this is actually a custom module (has module.yaml)
- const moduleYamlPath = path.join(cachedPath, 'module.yaml');
- if (await fs.pathExists(moduleYamlPath)) {
- // For quick update, we always rebuild from cache
- customModuleSources.set(moduleId, {
- id: moduleId,
- name: moduleId, // We'll read the actual name if needed
- sourcePath: cachedPath,
- cached: true, // Flag to indicate this is from cache
- });
- }
- }
- }
- }
-
- // Load saved IDE configurations
- const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
-
- // Get available modules (what we have source for)
- const availableModulesData = await this.moduleManager.listAvailable();
- const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
-
- // Add custom modules from manifest if their sources exist
- for (const [moduleId, customModule] of customModuleSources) {
- // Use the absolute sourcePath
- const sourcePath = customModule.sourcePath;
-
- // Check if source exists at the recorded path
- if (
- sourcePath &&
- (await fs.pathExists(sourcePath)) && // Add to available modules if not already there
- !availableModules.some((m) => m.id === moduleId)
- ) {
- availableModules.push({
- id: moduleId,
- name: customModule.name || moduleId,
- path: sourcePath,
- isCustom: true,
- fromManifest: true,
- });
- }
- }
-
- // Handle missing custom module sources using shared method
- const customModuleResult = await this.handleMissingCustomSources(
- customModuleSources,
- bmadDir,
- projectRoot,
- 'update',
- installedModules,
- );
-
- const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
-
- const customModulesFromManifest = validCustomModules.map((m) => ({
- ...m,
- isCustom: true,
- hasUpdate: true,
- }));
-
- const allAvailableModules = [...availableModules, ...customModulesFromManifest];
- const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
-
- // Core module is special - never include it in update flow
- const nonCoreInstalledModules = installedModules.filter((id) => id !== 'core');
-
- // Only update modules that are BOTH installed AND available (we have source for)
- const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id));
- const skippedModules = nonCoreInstalledModules.filter((id) => !availableModuleIds.has(id));
-
- // Add custom modules that were kept without sources to the skipped modules
- // This ensures their agents are preserved in the manifest
- for (const keptModule of keptModulesWithoutSources) {
- if (!skippedModules.includes(keptModule)) {
- skippedModules.push(keptModule);
- }
- }
-
- spinner.succeed(`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(', ')}`));
- }
-
- // Load existing configs and collect new fields (if any)
- console.log(chalk.cyan('\n📋 Checking for new configuration options...'));
- await this.configCollector.loadExistingConfig(projectDir);
-
- let promptedForNewFields = false;
-
- // Check core config for new fields
- const corePrompted = await this.configCollector.collectModuleConfigQuick('core', projectDir, true);
- if (corePrompted) {
- promptedForNewFields = true;
- }
-
- // Check each module we're updating for new fields (NOT skipped modules)
- for (const moduleName of modulesToUpdate) {
- const modulePrompted = await this.configCollector.collectModuleConfigQuick(moduleName, projectDir, true);
- if (modulePrompted) {
- promptedForNewFields = true;
- }
- }
-
- if (!promptedForNewFields) {
- console.log(chalk.green('✓ All configuration is up to date, no new options to configure'));
- }
-
- // Add metadata
- this.configCollector.collectedConfig._meta = {
- version: require(path.join(getProjectRoot(), 'package.json')).version,
- installDate: new Date().toISOString(),
- lastModified: new Date().toISOString(),
- };
-
- // Build the config object for the installer
- const installConfig = {
- directory: projectDir,
- installCore: true,
- modules: modulesToUpdate, // Only update modules we have source for
- ides: configuredIdes,
- skipIde: configuredIdes.length === 0,
- coreConfig: this.configCollector.collectedConfig.core,
- actionType: 'install', // Use regular install flow
- _quickUpdate: true, // Flag to skip certain prompts
- _preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
- _savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer
- _customModuleSources: customModuleSources, // Pass custom module sources for updates
- _existingModules: installedModules, // Pass all installed modules for manifest generation
- };
-
- // Call the standard install method
- const result = await this.install(installConfig);
-
- // 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!');
- }
-
- return {
- success: true,
- moduleCount: modulesToUpdate.length + 1, // +1 for core
- hadNewFields: promptedForNewFields,
- modules: ['core', ...modulesToUpdate],
- skippedModules: skippedModules,
- ides: configuredIdes,
- };
- } catch (error) {
- spinner.fail('Quick update failed');
- throw error;
- }
- }
-
- /**
- * Compile agents with customizations only
- * @param {Object} config - Configuration with directory
- * @returns {Object} Compilation result
- */
- async compileAgents(config) {
- const ora = require('ora');
- const chalk = require('chalk');
- const { ModuleManager } = require('../modules/manager');
- const { getSourcePath } = require('../../../lib/project-root');
-
- const spinner = ora('Recompiling agents with customizations...').start();
-
- try {
- const projectDir = path.resolve(config.directory);
- const { bmadDir } = await this.findBmadDir(projectDir);
-
- // Check if bmad directory exists
- if (!(await fs.pathExists(bmadDir))) {
- spinner.fail('No BMAD installation found');
- throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
- }
-
- // Detect existing installation
- const existingInstall = await this.detector.detect(bmadDir);
- const installedModules = existingInstall.modules.map((m) => m.id);
-
- // Initialize module manager
- const moduleManager = new ModuleManager();
- moduleManager.setBmadFolderName(path.basename(bmadDir));
-
- let totalAgentCount = 0;
-
- // Get custom module sources from cache
- const customModuleSources = new Map();
- const cacheDir = path.join(bmadDir, '_config', 'custom');
- if (await fs.pathExists(cacheDir)) {
- const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
-
- for (const cachedModule of cachedModules) {
- if (cachedModule.isDirectory()) {
- const moduleId = cachedModule.name;
- const cachedPath = path.join(cacheDir, moduleId);
- const moduleYamlPath = path.join(cachedPath, 'module.yaml');
-
- // Check if this is actually a custom module
- if (await fs.pathExists(moduleYamlPath)) {
- customModuleSources.set(moduleId, cachedPath);
- }
- }
- }
- }
-
- // Process each installed module
- for (const moduleId of installedModules) {
- spinner.text = `Recompiling agents in ${moduleId}...`;
-
- // Get source path
- let sourcePath;
- if (moduleId === 'core') {
- sourcePath = getSourcePath('core');
- } else {
- // First check if it's in the custom cache
- if (customModuleSources.has(moduleId)) {
- sourcePath = customModuleSources.get(moduleId);
- } else {
- sourcePath = await moduleManager.findModuleSource(moduleId);
- }
- }
-
- if (!sourcePath) {
- console.log(chalk.yellow(` Warning: Source not found for module ${moduleId}, skipping...`));
- continue;
- }
-
- const targetPath = path.join(bmadDir, moduleId);
-
- // Compile agents for this module
- await moduleManager.compileModuleAgents(sourcePath, targetPath, moduleId, bmadDir, this);
-
- // Count agents (rough estimate based on files)
- const agentsPath = path.join(targetPath, 'agents');
- if (await fs.pathExists(agentsPath)) {
- const agentFiles = await fs.readdir(agentsPath);
- const agentCount = agentFiles.filter(f => f.endsWith('.md')).length;
- totalAgentCount += agentCount;
- }
- }
-
- spinner.succeed('Agent recompilation complete!');
-
- return {
- success: true,
- agentCount: totalAgentCount,
- modules: installedModules,
- };
- } catch (error) {
- spinner.fail('Agent recompilation failed');
- throw error;
- }
- }
-
- /**
- * Private: Prompt for update action
- */
- async promptUpdateAction() {
- const inquirer = require('inquirer');
- return await inquirer.prompt([
- {
- type: 'list',
- name: 'action',
- message: 'What would you like to do?',
- choices: [{ name: 'Update existing installation', value: 'update' }],
- },
- ]);
- }
-
- /**
- * Handle legacy BMAD v4 migration with automatic backup
- * @param {string} projectDir - Project directory
- * @param {Object} legacyV4 - Legacy V4 detection result with offenders array
- */
- async handleLegacyV4Migration(projectDir, legacyV4) {
- console.log(chalk.yellow.bold('\n⚠️ Legacy BMAD v4 detected'));
- console.log(chalk.dim('The installer found legacy artefacts in your project.\n'));
-
- // Separate _bmad* folders (auto-backup) from other offending paths (manual cleanup)
- const bmadFolders = legacyV4.offenders.filter((p) => {
- const name = path.basename(p);
- return name.startsWith('_bmad'); // Only dot-prefixed folders get auto-backed up
- });
- const otherOffenders = legacyV4.offenders.filter((p) => {
- const name = path.basename(p);
- return !name.startsWith('_bmad'); // Everything else is manual cleanup
- });
-
- const inquirer = require('inquirer');
-
- // Show warning for other offending paths FIRST
- if (otherOffenders.length > 0) {
- console.log(chalk.yellow('⚠️ Recommended cleanup:'));
- console.log(chalk.dim('It is recommended to remove the following items before proceeding:\n'));
- for (const p of otherOffenders) console.log(chalk.dim(` - ${p}`));
-
- console.log(chalk.cyan('\nCleanup commands you can copy/paste:'));
- console.log(chalk.dim('macOS/Linux:'));
- for (const p of otherOffenders) console.log(chalk.dim(` rm -rf '${p}'`));
- console.log(chalk.dim('Windows:'));
- for (const p of otherOffenders) console.log(chalk.dim(` rmdir /S /Q "${p}"`));
-
- const { cleanedUp } = await inquirer.prompt([
- {
- type: 'confirm',
- name: 'cleanedUp',
- message: 'Have you completed the recommended cleanup? (You can proceed without it, but it is recommended)',
- default: false,
- },
- ]);
-
- if (cleanedUp) {
- console.log(chalk.green('✓ Cleanup acknowledged\n'));
- } else {
- console.log(chalk.yellow('⚠️ Proceeding without recommended cleanup\n'));
- }
- }
-
- // Handle _bmad* folders with automatic backup
- if (bmadFolders.length > 0) {
- console.log(chalk.cyan('The following legacy folders will be moved to v4-backup:'));
- for (const p of bmadFolders) console.log(chalk.dim(` - ${p}`));
-
- const { proceed } = await inquirer.prompt([
- {
- type: 'confirm',
- name: 'proceed',
- message: 'Proceed with backing up legacy v4 folders?',
- default: true,
- },
- ]);
-
- if (proceed) {
- const backupDir = path.join(projectDir, 'v4-backup');
- await fs.ensureDir(backupDir);
-
- for (const folder of bmadFolders) {
- const folderName = path.basename(folder);
- const backupPath = path.join(backupDir, folderName);
-
- // If backup already exists, add timestamp
- let finalBackupPath = backupPath;
- if (await fs.pathExists(backupPath)) {
- const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-').split('T')[0];
- finalBackupPath = path.join(backupDir, `${folderName}-${timestamp}`);
- }
-
- await fs.move(folder, finalBackupPath, { overwrite: false });
- console.log(chalk.green(`✓ Moved ${folderName} to ${path.relative(projectDir, finalBackupPath)}`));
- }
- } else {
- throw new Error('Installation cancelled by user');
- }
- }
- }
-
- /**
- * Read files-manifest.csv
- * @param {string} bmadDir - BMAD installation directory
- * @returns {Array} Array of file entries from files-manifest.csv
- */
- async readFilesManifest(bmadDir) {
- const filesManifestPath = path.join(bmadDir, '_config', 'files-manifest.csv');
- if (!(await fs.pathExists(filesManifestPath))) {
- return [];
- }
-
- try {
- const content = await fs.readFile(filesManifestPath, 'utf8');
- const lines = content.split('\n');
- const files = [];
-
- for (let i = 1; i < lines.length; i++) {
- // Skip header
- const line = lines[i].trim();
- if (!line) continue;
-
- // Parse CSV line properly handling quoted values
- const parts = [];
- let current = '';
- let inQuotes = false;
-
- for (const char of line) {
- if (char === '"') {
- inQuotes = !inQuotes;
- } else if (char === ',' && !inQuotes) {
- parts.push(current);
- current = '';
- } else {
- current += char;
- }
- }
- parts.push(current); // Add last part
-
- if (parts.length >= 4) {
- files.push({
- type: parts[0],
- name: parts[1],
- module: parts[2],
- path: parts[3],
- hash: parts[4] || null, // Hash may not exist in old manifests
- });
- }
- }
-
- return files;
- } catch (error) {
- console.warn('Warning: Could not read files-manifest.csv:', error.message);
- return [];
- }
- }
-
- /**
- * Detect custom and modified files
- * @param {string} bmadDir - BMAD installation directory
- * @param {Array} existingFilesManifest - Previous files from files-manifest.csv
- * @returns {Object} Object with customFiles and modifiedFiles arrays
- */
- async detectCustomFiles(bmadDir, existingFilesManifest) {
- const customFiles = [];
- const modifiedFiles = [];
-
- // Memory is always in _bmad/_memory
- const bmadMemoryPath = '_memory';
-
- // Check if the manifest has hashes - if not, we can't detect modifications
- let manifestHasHashes = false;
- if (existingFilesManifest && existingFilesManifest.length > 0) {
- manifestHasHashes = existingFilesManifest.some((f) => f.hash);
- }
-
- // Build map of previously installed files from files-manifest.csv with their hashes
- const installedFilesMap = new Map();
- for (const fileEntry of existingFilesManifest) {
- if (fileEntry.path) {
- const absolutePath = path.join(bmadDir, fileEntry.path);
- installedFilesMap.set(path.normalize(absolutePath), {
- hash: fileEntry.hash,
- relativePath: fileEntry.path,
- });
- }
- }
-
- // Recursively scan bmadDir for all files
- const scanDirectory = async (dir) => {
- try {
- const entries = await fs.readdir(dir, { withFileTypes: true });
- for (const entry of entries) {
- const fullPath = path.join(dir, entry.name);
-
- if (entry.isDirectory()) {
- // Skip certain directories
- if (entry.name === 'node_modules' || entry.name === '.git') {
- continue;
- }
- await scanDirectory(fullPath);
- } else if (entry.isFile()) {
- const normalizedPath = path.normalize(fullPath);
- const fileInfo = installedFilesMap.get(normalizedPath);
-
- // Skip certain system files that are auto-generated
- const relativePath = path.relative(bmadDir, fullPath);
- const fileName = path.basename(fullPath);
-
- // Skip _config directory EXCEPT for modified agent customizations
- if (relativePath.startsWith('_config/') || relativePath.startsWith('_config\\')) {
- // Special handling for .customize.yaml files - only preserve if modified
- if (relativePath.includes('/agents/') && fileName.endsWith('.customize.yaml')) {
- // Check if the customization file has been modified from manifest
- const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
- if (await fs.pathExists(manifestPath)) {
- const crypto = require('node:crypto');
- const currentContent = await fs.readFile(fullPath, 'utf8');
- const currentHash = crypto.createHash('sha256').update(currentContent).digest('hex');
-
- const yaml = require('yaml');
- const manifestContent = await fs.readFile(manifestPath, 'utf8');
- const manifestData = yaml.parse(manifestContent);
- const originalHash = manifestData.agentCustomizations?.[relativePath];
-
- // Only add to customFiles if hash differs (user modified)
- if (originalHash && currentHash !== originalHash) {
- customFiles.push(fullPath);
- }
- }
- }
- continue;
- }
-
- if (relativePath.startsWith(bmadMemoryPath + '/') && path.dirname(relativePath).includes('-sidecar')) {
- continue;
- }
-
- // Skip config.yaml files - these are regenerated on each install/update
- if (fileName === 'config.yaml') {
- continue;
- }
-
- if (!fileInfo) {
- // File not in manifest = custom file
- // EXCEPT: Agent .md files in module folders are generated files, not custom
- // Only treat .md files under _config/agents/ as custom
- if (!(fileName.endsWith('.md') && relativePath.includes('/agents/') && !relativePath.startsWith('_config/'))) {
- customFiles.push(fullPath);
- }
- } else if (manifestHasHashes && fileInfo.hash) {
- // File in manifest with hash - check if it was modified
- const currentHash = await this.manifest.calculateFileHash(fullPath);
- if (currentHash && currentHash !== fileInfo.hash) {
- // Hash changed = file was modified
- modifiedFiles.push({
- path: fullPath,
- relativePath: fileInfo.relativePath,
- });
- }
- }
- }
- }
- } catch {
- // Ignore errors scanning directories
- }
- };
-
- await scanDirectory(bmadDir);
- return { customFiles, modifiedFiles };
- }
-
- /**
- * Private: Create agent configuration files
- * @param {string} bmadDir - BMAD installation directory
- * @param {Object} userInfo - User information including name and language
- */
- async createAgentConfigs(bmadDir, userInfo = null) {
- const agentConfigDir = path.join(bmadDir, '_config', 'agents');
- await fs.ensureDir(agentConfigDir);
-
- // Get all agents from all modules
- const agents = [];
- const agentDetails = []; // For manifest generation
-
- // Check modules for agents (including core)
- const entries = await fs.readdir(bmadDir, { withFileTypes: true });
- for (const entry of entries) {
- if (entry.isDirectory() && entry.name !== '_config') {
- const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents');
- if (await fs.pathExists(moduleAgentsPath)) {
- const agentFiles = await fs.readdir(moduleAgentsPath);
- for (const agentFile of agentFiles) {
- if (agentFile.endsWith('.md')) {
- const agentPath = path.join(moduleAgentsPath, agentFile);
- const agentContent = await fs.readFile(agentPath, 'utf8');
-
- // Skip agents with localskip="true"
- const hasLocalSkip = agentContent.match(/]*\slocalskip="true"[^>]*>/);
- if (hasLocalSkip) {
- continue; // Skip this agent - it should not have been installed
- }
-
- const agentName = path.basename(agentFile, '.md');
-
- // Extract any nodes with agentConfig="true"
- const agentConfigNodes = this.extractAgentConfigNodes(agentContent);
-
- agents.push({
- name: agentName,
- module: entry.name,
- agentConfigNodes: agentConfigNodes,
- });
-
- // Use shared AgentPartyGenerator to extract details
- let details = AgentPartyGenerator.extractAgentDetails(agentContent, entry.name, agentName);
-
- // Apply config overrides if they exist
- if (details) {
- const configPath = path.join(agentConfigDir, `${entry.name}-${agentName}.md`);
- if (await fs.pathExists(configPath)) {
- const configContent = await fs.readFile(configPath, 'utf8');
- details = AgentPartyGenerator.applyConfigOverrides(details, configContent);
- }
- agentDetails.push(details);
- }
- }
- }
- }
- }
- }
-
- // Create config file for each agent
- let createdCount = 0;
- let skippedCount = 0;
-
- // Load agent config template
- const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md');
- const templateContent = await fs.readFile(templatePath, 'utf8');
-
- for (const agent of agents) {
- const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`);
-
- // Skip if config file already exists (preserve custom configurations)
- if (await fs.pathExists(configPath)) {
- skippedCount++;
- continue;
- }
-
- // Build config content header
- let configContent = `# Agent Config: ${agent.name}\n\n`;
-
- // Process template and add agent-specific config nodes
- let processedTemplate = templateContent;
-
- // Replace {core:user_name} placeholder with actual user name if available
- if (userInfo && userInfo.userName) {
- processedTemplate = processedTemplate.replaceAll('{core:user_name}', userInfo.userName);
- }
-
- // Replace {core:communication_language} placeholder with actual language if available
- if (userInfo && userInfo.responseLanguage) {
- processedTemplate = processedTemplate.replaceAll('{core:communication_language}', userInfo.responseLanguage);
- }
-
- // If this agent has agentConfig nodes, add them after the existing comment
- if (agent.agentConfigNodes && agent.agentConfigNodes.length > 0) {
- // Find the agent-specific configuration nodes comment
- const commentPattern = /(\s*)/;
- const commentMatch = processedTemplate.match(commentPattern);
-
- if (commentMatch) {
- // Add nodes right after the comment
- let agentSpecificNodes = '';
- for (const node of agent.agentConfigNodes) {
- agentSpecificNodes += `\n ${node}`;
- }
-
- processedTemplate = processedTemplate.replace(commentPattern, `$1${agentSpecificNodes}`);
- }
- }
-
- configContent += processedTemplate;
-
- // Ensure POSIX-compliant final newline
- if (!configContent.endsWith('\n')) {
- configContent += '\n';
- }
-
- await fs.writeFile(configPath, configContent, 'utf8');
- this.installedFiles.add(configPath); // Track agent config files
- createdCount++;
- }
-
- // Generate agent manifest with overrides applied
- await this.generateAgentManifest(bmadDir, agentDetails);
-
- return { total: agents.length, created: createdCount, skipped: skippedCount };
- }
-
- /**
- * Generate agent manifest XML file
- * @param {string} bmadDir - BMAD installation directory
- * @param {Array} agentDetails - Array of agent details
- */
- async generateAgentManifest(bmadDir, agentDetails) {
- const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
- await AgentPartyGenerator.writeAgentParty(manifestPath, agentDetails, { forWeb: false });
- }
-
- /**
- * Extract nodes with agentConfig="true" from agent content
- * @param {string} content - Agent file content
- * @returns {Array} Array of XML nodes that should be added to agent config
- */
- extractAgentConfigNodes(content) {
- const nodes = [];
-
- try {
- // Find all XML nodes with agentConfig="true"
- // Match self-closing tags and tags with content
- const selfClosingPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*\/>/g;
- const withContentPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*>([\s\S]*?)<\/\1>/g;
-
- // Extract self-closing tags
- let match;
- while ((match = selfClosingPattern.exec(content)) !== null) {
- // Extract just the tag without children (structure only)
- const tagMatch = match[0].match(/<([a-zA-Z][a-zA-Z0-9_-]*)([^>]*)\/>/);
- if (tagMatch) {
- const tagName = tagMatch[1];
- const attributes = tagMatch[2].replace(/\s*agentConfig="true"/, ''); // Remove agentConfig attribute
- nodes.push(`<${tagName}${attributes}>${tagName}>`);
- }
- }
-
- // Extract tags with content
- while ((match = withContentPattern.exec(content)) !== null) {
- const fullMatch = match[0];
- const tagName = match[1];
-
- // Extract opening tag with attributes (removing agentConfig="true")
- const openingTagMatch = fullMatch.match(new RegExp(`<${tagName}([^>]*)>`));
- if (openingTagMatch) {
- const attributes = openingTagMatch[1].replace(/\s*agentConfig="true"/, '');
- // Add empty node structure (no children)
- nodes.push(`<${tagName}${attributes}>${tagName}>`);
- }
- }
- } catch (error) {
- console.error('Error extracting agentConfig nodes:', error);
- }
-
- return nodes;
- }
-
- /**
- * Handle missing custom module sources interactively
- * @param {Map} customModuleSources - Map of custom module ID to info
- * @param {string} bmadDir - BMAD directory
- * @param {string} projectRoot - Project root directory
- * @param {string} operation - Current operation ('update', 'compile', etc.)
- * @param {Array} installedModules - Array of installed module IDs (will be modified)
- * @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
- */
- async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules) {
- const validCustomModules = [];
- const keptModulesWithoutSources = []; // Track modules kept without sources
- const customModulesWithMissingSources = [];
-
- // Check which sources exist
- for (const [moduleId, customInfo] of customModuleSources) {
- if (await fs.pathExists(customInfo.sourcePath)) {
- validCustomModules.push({
- id: moduleId,
- name: customInfo.name,
- path: customInfo.sourcePath,
- info: customInfo,
- });
- } else {
- // For cached modules that are missing, we just skip them without prompting
- if (customInfo.cached) {
- // Skip cached modules without prompting
- keptModulesWithoutSources.push({
- id: moduleId,
- name: customInfo.name,
- cached: true,
- });
- } else {
- customModulesWithMissingSources.push({
- id: moduleId,
- name: customInfo.name,
- sourcePath: customInfo.sourcePath,
- relativePath: customInfo.relativePath,
- info: customInfo,
- });
- }
- }
- }
-
- // If no missing sources, return immediately
- if (customModulesWithMissingSources.length === 0) {
- return {
- validCustomModules,
- keptModulesWithoutSources: [],
- };
- }
-
- // 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:`));
-
- const inquirer = require('inquirer');
- 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}`));
-
- const choices = [
- {
- name: 'Keep installed (will not be processed)',
- value: 'keep',
- short: 'Keep',
- },
- {
- name: 'Specify new source location',
- value: 'update',
- short: 'Update',
- },
- ];
-
- // Only add remove option if not just compiling agents
- if (operation !== 'compile-agents') {
- choices.push({
- name: '⚠️ REMOVE module completely (destructive!)',
- value: 'remove',
- short: 'Remove',
- });
- }
-
- const { action } = await inquirer.prompt([
- {
- type: 'list',
- name: 'action',
- message: `How would you like to handle "${missing.name}"?`,
- choices,
- },
- ]);
-
- switch (action) {
- case 'update': {
- const { newSourcePath } = await inquirer.prompt([
- {
- type: 'input',
- name: 'newSourcePath',
- message: 'Enter the new path to the custom module:',
- default: missing.sourcePath,
- validate: async (input) => {
- if (!input || input.trim() === '') {
- return 'Please enter a path';
- }
- const expandedPath = path.resolve(input.trim());
- if (!(await fs.pathExists(expandedPath))) {
- return 'Path does not exist';
- }
- // Check if it looks like a valid module
- const moduleYamlPath = path.join(expandedPath, 'module.yaml');
- const agentsPath = path.join(expandedPath, 'agents');
- const workflowsPath = path.join(expandedPath, 'workflows');
-
- if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) {
- return 'Path does not appear to contain a valid custom module';
- }
- return true;
- },
- },
- ]);
-
- // Update the source in manifest
- const resolvedPath = path.resolve(newSourcePath.trim());
- missing.info.sourcePath = resolvedPath;
- // Remove relativePath - we only store absolute sourcePath now
- delete missing.info.relativePath;
- await this.manifest.addCustomModule(bmadDir, missing.info);
-
- validCustomModules.push({
- id: moduleId,
- name: missing.name,
- path: resolvedPath,
- info: missing.info,
- });
-
- updatedCount++;
- console.log(chalk.green(`✓ 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, moduleId)}`));
-
- const { confirm } = await inquirer.prompt([
- {
- type: 'confirm',
- name: 'confirm',
- message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
- default: false,
- },
- ]);
-
- if (confirm) {
- const { typedConfirm } = await inquirer.prompt([
- {
- type: 'input',
- name: 'typedConfirm',
- message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
- validate: (input) => {
- if (input !== 'DELETE') {
- return chalk.red('You must type "DELETE" exactly to proceed');
- }
- return true;
- },
- },
- ]);
-
- if (typedConfirm === 'DELETE') {
- // Remove the module from filesystem and manifest
- const modulePath = path.join(bmadDir, moduleId);
- 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 this.manifest.removeModule(bmadDir, moduleId);
- await this.manifest.removeCustomModule(bmadDir, moduleId);
- console.log(chalk.yellow(` ✓ Removed from manifest`));
-
- // Also remove from installedModules list
- if (installedModules && installedModules.includes(moduleId)) {
- const index = installedModules.indexOf(moduleId);
- if (index !== -1) {
- installedModules.splice(index, 1);
- }
- }
-
- removedCount++;
- console.log(chalk.red.bold(`✓ "${missing.name}" has been permanently removed`));
- } else {
- console.log(chalk.dim(' Removal cancelled - module will be kept'));
- keptCount++;
- }
- } else {
- console.log(chalk.dim(' Removal cancelled - module will be kept'));
- keptCount++;
- }
-
- break;
- }
- case 'keep': {
- keptCount++;
- keptModulesWithoutSources.push(moduleId);
- console.log(chalk.dim(` Module will be kept as-is`));
-
- break;
- }
- // No default
- }
- }
-
- // 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`));
- }
-
- return {
- validCustomModules,
- keptModulesWithoutSources,
- };
- }
-}
-
-module.exports = { Installer };
diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js
index e54bf3be..4844f243 100644
--- a/tools/cli/installers/lib/modules/manager.js
+++ b/tools/cli/installers/lib/modules/manager.js
@@ -731,7 +731,7 @@ class ModuleManager {
async compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir, installer = null) {
const sourceAgentsPath = path.join(sourcePath, 'agents');
const targetAgentsPath = path.join(targetPath, 'agents');
- const cfgAgentsDir = path.join(bmadDir, '_bmad', '_config', 'agents');
+ const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
// Check if agents directory exists in source
if (!(await fs.pathExists(sourceAgentsPath))) {