Compare commits
14 Commits
aee3a3f209
...
00c24eaefa
| Author | SHA1 | Date |
|---|---|---|
|
|
00c24eaefa | |
|
|
eb4325fab9 | |
|
|
57ceaf9fa9 | |
|
|
1513b2d478 | |
|
|
2da016f797 | |
|
|
6947851393 | |
|
|
9d7b09d065 | |
|
|
86f2786dde | |
|
|
a638f062b9 | |
|
|
738237b4ae | |
|
|
6430173738 | |
|
|
baaa984a90 | |
|
|
3727eca844 | |
|
|
9992b0e56e |
544
CHANGELOG.md
544
CHANGELOG.md
|
|
@ -1,5 +1,75 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [6.0.0-alpha.15]
|
||||||
|
|
||||||
|
**Release: December 7, 2025**
|
||||||
|
|
||||||
|
### 🔧 Module Installation Standardization
|
||||||
|
|
||||||
|
**Unified Module Configuration:**
|
||||||
|
|
||||||
|
- **module.yaml Standard**: All modules now use `module.yaml` instead of `_module-installer/install-config.yaml` for consistent configuration (BREAKING CHANGE)
|
||||||
|
- **Universal Installer**: Both core and custom modules now use the same installer with consistent behavior
|
||||||
|
- **Streamlined Module Creation**: Module builder templates updated to use new module.yaml standard
|
||||||
|
- **Enhanced Module Discovery**: Improved module caching and discovery mechanisms
|
||||||
|
|
||||||
|
**Custom Content Installation Revolution:**
|
||||||
|
|
||||||
|
- **Interactive Custom Content Search**: Installer now proactively asks if you have custom content to install
|
||||||
|
- **Flexible Location Specification**: Users can indicate custom content location during installation
|
||||||
|
- **Improved Custom Module Handler**: Enhanced error handling and debug output for custom installations
|
||||||
|
- **Comprehensive Documentation**: New custom-content-installation.md guide (245 lines) replacing custom-agent-installation.md
|
||||||
|
|
||||||
|
### 🤖 Code Review Integration Expansion
|
||||||
|
|
||||||
|
**AI Review Tools:**
|
||||||
|
|
||||||
|
- **CodeRabbit AI Integration**: Added .coderabbit.yaml configuration for automated code review
|
||||||
|
- **Raven's Verdict PR Review Tool**: New PR review automation tool (297 lines of documentation)
|
||||||
|
- **Review Path Configuration**: Proper exclusion patterns for node_modules and generated files
|
||||||
|
- **Review Documentation**: Comprehensive usage guidance and skip conditions for PRs
|
||||||
|
|
||||||
|
### 📚 Documentation Improvements
|
||||||
|
|
||||||
|
**Documentation Restructuring:**
|
||||||
|
|
||||||
|
- **Code of Conduct**: Moved to .github/ folder following GitHub standards
|
||||||
|
- **Gem Creation Link**: Updated to point to Gemini Gem manager instead of deprecated interface
|
||||||
|
- **Example Custom Content**: Improved README files and disabled example modules to prevent accidental installation
|
||||||
|
- **Custom Module Documentation**: Enhanced module installation guides with new YAML structure
|
||||||
|
|
||||||
|
### 🧹 Cleanup & Optimization
|
||||||
|
|
||||||
|
**Memory Management:**
|
||||||
|
|
||||||
|
- **Removed Hardcoded .bmad Folders**: Cleaned up demo content to use configurable paths
|
||||||
|
- **Sidecar File Cleanup**: Removed old .bmad-user-memory folders from wellness modules
|
||||||
|
- **Example Content Organization**: Better organization of example-custom-content directory
|
||||||
|
|
||||||
|
**Installer Improvements:**
|
||||||
|
|
||||||
|
- **Debug Output Enhancement**: Added informative debug output when installer encounters errors
|
||||||
|
- **Custom Module Caching**: Improved caching mechanism for custom module installations
|
||||||
|
- **Consistent Behavior**: All modules now behave consistently regardless of custom or core status
|
||||||
|
|
||||||
|
### 📊 Statistics
|
||||||
|
|
||||||
|
- **77 files changed** with 2,852 additions and 607 deletions
|
||||||
|
- **15 commits** since alpha.14
|
||||||
|
|
||||||
|
### ⚠️ Breaking Changes
|
||||||
|
|
||||||
|
1. **module.yaml Configuration**: All modules must now use `module.yaml` instead of `_module-installer/install-config.yaml`
|
||||||
|
- Core modules updated automatically
|
||||||
|
- Custom modules will need to rename their configuration file
|
||||||
|
- Module builder templates generate new format
|
||||||
|
|
||||||
|
### 📦 New Dependencies
|
||||||
|
|
||||||
|
- No new dependencies added in this release
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [6.0.0-alpha.14]
|
## [6.0.0-alpha.14]
|
||||||
|
|
||||||
**Release: December 7, 2025**
|
**Release: December 7, 2025**
|
||||||
|
|
@ -101,159 +171,29 @@
|
||||||
|
|
||||||
### 🏗️ Revolutionary Workflow Architecture
|
### 🏗️ Revolutionary Workflow Architecture
|
||||||
|
|
||||||
**Granular Step-File Workflow System (NEW in alpha.13):**
|
- **Step-File System**: Complete conversion to granular step-file architecture with dynamic menu generation
|
||||||
|
- **Phase 4 Transformation**: Simplified architecture with sprint planning integration (Jira, Linear, Trello)
|
||||||
- **Multi-Menu Support**: Workflows now support granular step-file architecture with dynamic menu generation
|
- **Performance Improvements**: Eliminated time-based estimates, reduced file loading times
|
||||||
- **Sharded Workflows**: Complete conversion of Phase 1 and 2 workflows to stepwise sharded architecture
|
- **Legacy Cleanup**: Removed all deprecated workflows for cleaner system
|
||||||
- **Improved Performance**: Reduced file loading times and eliminated time-based estimates throughout
|
|
||||||
- **Workflow Builder**: New dedicated workflow builder for creating stepwise workflows
|
|
||||||
- **PRD Workflow**: First completely reworked sharded workflow resolving Sonnet compatibility issues
|
|
||||||
|
|
||||||
**Core Workflow Transformations:**
|
|
||||||
|
|
||||||
- Phase 1 and 2 workflows completely converted to sharded step-flow architecture
|
|
||||||
- UX Design workflow converted to sharded step workflow
|
|
||||||
- Brainstorming, Research, and Party Mode updated to use sharded step-flow workflows
|
|
||||||
- Architecture workflows enhanced with step sharding and performance improvements
|
|
||||||
|
|
||||||
### 🎯 Code Review & Development Enhancement
|
|
||||||
|
|
||||||
**Advanced Code Review System:**
|
|
||||||
|
|
||||||
- **Adversarial Code Review**: Quick-dev workflow now recommends adversarial review approach for higher quality
|
|
||||||
- **Multi-LLM Strategy**: Dev-story workflow recommends different LLM models for code review tasks
|
|
||||||
- **Agent Compiler Optimization**: Complete handler cleanup and performance improvements
|
|
||||||
|
|
||||||
### 🤖 Agent System Revolution
|
### 🤖 Agent System Revolution
|
||||||
|
|
||||||
**Universal Custom Agent Support:**
|
- **Universal Custom Agent Support**: Extended to ALL IDEs including Antigravity and Rovo Dev
|
||||||
|
- **Agent Creation Workflow**: Enhanced with better documentation and parameter clarity
|
||||||
|
- **Multi-Source Discovery**: Agents now check multiple source locations for better discovery
|
||||||
|
- **GitHub Migration**: Integration moved from chatmodes to agents folder
|
||||||
|
|
||||||
- **Complete IDE Coverage**: Custom agent support extended to ALL remaining IDEs
|
### 🧪 Testing Infrastructure
|
||||||
- **Antigravity IDE Integration**: Added custom agent support with proper gitignore configuration
|
|
||||||
- **Multiple Source Locations**: Compile agents now checks multiple source locations for better discovery
|
|
||||||
- **Persona Name Display**: Fixed proper persona names display in custom agent manifests
|
|
||||||
- **New IDE Support**: Added support for Rovo Dev IDE
|
|
||||||
|
|
||||||
**Agent Creation & Management:**
|
- **Playwright Utils Integration**: @seontechnologies/playwright-utils across all testing workflows
|
||||||
|
- **TTS Injection System**: Complete text-to-speech integration for voice feedback
|
||||||
- **Improved Creation Workflow**: Enhanced agent creation workflow with better documentation
|
- **Web Bundle Test Support**: Enabled web bundles for test environments
|
||||||
- **Parameter Clarity**: Renamed agent-install parameters for better understanding
|
|
||||||
- **Menu Organization**: BMad Agents menu items logically ordered with optional/recommended/required tags
|
|
||||||
- **GitHub Migration**: GitHub integration now uses agents folder instead of chatmodes
|
|
||||||
|
|
||||||
### 🔧 Phase 4 & Sprint Evolution
|
|
||||||
|
|
||||||
**Complete Phase 4 Transformation:**
|
|
||||||
|
|
||||||
- **Simplified Architecture**: Phase 4 workflows completely transformed - simpler, faster, better results
|
|
||||||
- **Sprint Planning Integration**: Unified sprint planning with placeholders for Jira, Linear, and Trello integration
|
|
||||||
- **Status Management**: Better status loading and updating for Phase 4 artifacts
|
|
||||||
- **Workflow Reduction**: Phase 4 streamlined to single sprint planning item with clear validation
|
|
||||||
- **Dynamic Workflows**: All Level 1-3 workflows now dynamically suggest next steps based on context
|
|
||||||
|
|
||||||
### 🧪 Testing Infrastructure Expansion
|
|
||||||
|
|
||||||
**Playwright Utils Integration:**
|
|
||||||
|
|
||||||
- Test Architect now supports `@seontechnologies/playwright-utils` integration
|
|
||||||
- Installation prompt with `use_playwright_utils` configuration flag
|
|
||||||
- 11 comprehensive knowledge fragments covering ALL utilities
|
|
||||||
- Adaptive workflow recommendations across 6 testing workflows
|
|
||||||
- Production-ready utilities from SEON Technologies integrated with TEA patterns
|
|
||||||
|
|
||||||
**Testing Environment:**
|
|
||||||
|
|
||||||
- **Web Bundle Support**: Enabled web bundles for test and development environments
|
|
||||||
- **Test Architecture**: Enhanced test design for architecture level (Phase 3) testing
|
|
||||||
|
|
||||||
### 📦 Installation & Configuration
|
|
||||||
|
|
||||||
**Installer Improvements:**
|
|
||||||
|
|
||||||
- **Cleanup Options**: Installer now allows cleanup of unneeded files during upgrades
|
|
||||||
- **Username Default**: Installer now defaults to system username for better UX
|
|
||||||
- **IDE Selection**: Added empty IDE selection warning and promoted Antigravity to recommended
|
|
||||||
- **NPM Vulnerabilities**: Resolved all npm vulnerabilities for enhanced security
|
|
||||||
- **Documentation Installation**: Made documentation installation optional to reduce footprint
|
|
||||||
|
|
||||||
**Text-to-Speech from AgentVibes optional Integration:**
|
|
||||||
|
|
||||||
- **TTS_INJECTION System**: Complete text-to-speech integration via injection system
|
|
||||||
- **Agent Vibes**: Enhanced with TTS capabilities for voice feedback
|
|
||||||
|
|
||||||
### 🛠️ Tool & IDE Updates
|
|
||||||
|
|
||||||
**IDE Tool Enhancements:**
|
|
||||||
|
|
||||||
- **GitHub Copilot**: Fixed tool names consistency across workflows
|
|
||||||
- **KiloCode Integration**: Gave kilocode tool proper access to bmad modes
|
|
||||||
- **Code Quality**: Added radix parameter to parseInt() calls for better reliability
|
|
||||||
- **Agent Menu Optimization**: Improved agent performance in Claude Code slash commands
|
|
||||||
|
|
||||||
### 📚 Documentation & Standards
|
|
||||||
|
|
||||||
**Documentation Cleanup:**
|
|
||||||
|
|
||||||
- **Installation Guide**: Removed fluff and updated with npx support
|
|
||||||
- **Workflow Documentation**: Fixed documentation by removing non-existent workflows and Mermaid diagrams
|
|
||||||
- **Phase Numbering**: Fixed phase numbering consistency throughout documentation
|
|
||||||
- **Package References**: Corrected incorrect npm package references
|
|
||||||
|
|
||||||
**Workflow Compliance:**
|
|
||||||
|
|
||||||
- **Validation Checks**: Enhanced workflow validation checks for compliance
|
|
||||||
- **Product Brief**: Updated to comply with documented workflow standards
|
|
||||||
- **Status Integration**: Workflow-status can now call workflow-init for better integration
|
|
||||||
|
|
||||||
### 🔍 Legacy Workflow Cleanup
|
|
||||||
|
|
||||||
**Deprecated Workflows Removed:**
|
|
||||||
|
|
||||||
- **Audit Workflow**: Completely removed audit workflow and all associated files
|
|
||||||
- **Convert Legacy**: Removed legacy conversion utilities
|
|
||||||
- **Create/Edit Workflows**: Removed old workflow creation and editing workflows
|
|
||||||
- **Clean Architecture**: Simplified workflow structure by removing deprecated legacy workflows
|
|
||||||
|
|
||||||
### 🐛 Technical Fixes
|
|
||||||
|
|
||||||
**System Improvements:**
|
|
||||||
|
|
||||||
- **File Path Handling**: Fixed various file path issues across workflows
|
|
||||||
- **Manifest Updates**: Updated manifest to use agents folder structure
|
|
||||||
- **Web Bundle Configuration**: Fixed web bundle configurations for better compatibility
|
|
||||||
- **CSV Column Mismatch**: Fixed manifest schema upgrade issues
|
|
||||||
|
|
||||||
### ⚠️ Breaking Changes
|
### ⚠️ Breaking Changes
|
||||||
|
|
||||||
**Workflow Architecture:**
|
1. **Legacy Workflows Removed**: Migrate to new stepwise sharded workflows
|
||||||
|
2. **Phase 4 Restructured**: Update automation expecting old Phase 4 structure
|
||||||
- All legacy workflows have been removed - ensure you're using the new stepwise sharded workflows
|
3. **Agent Compilation Required**: Custom agents must use new creation workflow
|
||||||
- Phase 4 completely restructured - update any automation expecting old Phase 4 structure
|
|
||||||
- Epic creation now requires architectural context (moved to Phase 3 in previous release)
|
|
||||||
|
|
||||||
**Agent System:**
|
|
||||||
|
|
||||||
- Custom agents now require proper compilation - use the new agent creation workflow
|
|
||||||
- GitHub integration moved from chatmodes to agents folder - update any references
|
|
||||||
|
|
||||||
### 📊 Impact Summary
|
|
||||||
|
|
||||||
**New in alpha.13:**
|
|
||||||
|
|
||||||
- **Stepwise Workflow Architecture**: Complete transformation of all workflows to granular step-file system
|
|
||||||
- **Universal Custom Agent Support**: Extended to ALL IDEs with improved creation workflow
|
|
||||||
- **Phase 4 Revolution**: Completely restructured with sprint planning integration
|
|
||||||
- **Legacy Cleanup**: Removed all deprecated workflows for cleaner system
|
|
||||||
- **Advanced Code Review**: New adversarial review approach with multi-LLM strategy
|
|
||||||
- **Text-to-Speech**: Full TTS integration for voice feedback
|
|
||||||
- **Testing Expansion**: Playwright utils integration across all testing workflows
|
|
||||||
|
|
||||||
**Enhanced from alpha.12:**
|
|
||||||
|
|
||||||
- **Performance**: Improved file loading and removed time-based estimates
|
|
||||||
- **Documentation**: Complete cleanup with accurate references
|
|
||||||
- **Installer**: Better UX with cleanup options and improved defaults
|
|
||||||
- **Agent System**: More reliable compilation and better persona handling
|
|
||||||
|
|
||||||
## [6.0.0-alpha.12]
|
## [6.0.0-alpha.12]
|
||||||
|
|
||||||
|
|
@ -267,313 +207,101 @@
|
||||||
|
|
||||||
**Release: November 18, 2025**
|
**Release: November 18, 2025**
|
||||||
|
|
||||||
This alpha release introduces a complete agent installation system with the new `bmad agent-install` command, vastly improves the BMB agent builder capabilities with comprehensive documentation and reference agents, and refines diagram distribution to better align with BMad Method's core principle: **BMad agents mirror real agile teams**.
|
### 🚀 Agent Installation Revolution
|
||||||
|
|
||||||
### 🎨 Diagram Capabilities Refined and Distributed
|
- **bmad agent-install CLI**: Interactive agent installation with persona customization
|
||||||
|
- **4 Reference Agents**: commit-poet, journal-keeper, security-engineer, trend-analyst
|
||||||
**Excalidraw Integration Evolution:**
|
- **Agent Compilation Engine**: YAML → XML with smart handler injection
|
||||||
|
- **60 Communication Presets**: Pure communication styles for agent personas
|
||||||
Building on the excellent Excalidraw integration introduced with the Frame Expert agent, we've refined how diagram capabilities are distributed across the BMad Method ecosystem to better reflect real agile team dynamics.
|
|
||||||
|
|
||||||
**The Refinement:**
|
|
||||||
|
|
||||||
- The valuable Excalidraw diagramming capabilities have been distributed to the agents who naturally create these artifacts in real teams
|
|
||||||
- **Architect**: System architecture diagrams, data flow visualizations
|
|
||||||
- **Product Manager**: Process flowcharts and workflow diagrams
|
|
||||||
- **UX Designer**: Wireframe creation capabilities
|
|
||||||
- **Tech Writer**: All diagram types for documentation needs
|
|
||||||
- **New CIS Agent**: presentation-master for specialized visual communication
|
|
||||||
|
|
||||||
**Shared Infrastructure Enhancement:**
|
|
||||||
|
|
||||||
- Excalidraw templates, component libraries, and validation patterns elevated to core resources
|
|
||||||
- Available to both BMM agents AND CIS presentation specialists
|
|
||||||
- Preserves all the excellent Excalidraw functionality while aligning with natural team roles
|
|
||||||
|
|
||||||
### 🚀 New Agent Installation System
|
|
||||||
|
|
||||||
**Agent Installation Infrastructure (NEW in alpha.11):**
|
|
||||||
|
|
||||||
- `bmad agent-install` CLI command with interactive persona customization
|
|
||||||
- **YAML → XML compilation engine** with smart handler injection
|
|
||||||
- Supports Simple (single file), Expert (with sidecars), and Module agents
|
|
||||||
- Handlebars-style template variable processing
|
|
||||||
- Automatic manifest tracking and IDE integration
|
|
||||||
- Source preservation in `_cfg/custom/agents/` for reinstallation
|
|
||||||
|
|
||||||
**New Reference Agents Added:**
|
|
||||||
|
|
||||||
- **commit-poet**: Poetic git commit message generator (Simple agent example)
|
|
||||||
- **journal-keeper**: Daily journaling agent with templates (Expert agent example)
|
|
||||||
- **security-engineer & trend-analyst**: Module agent examples with ecosystem integration
|
|
||||||
|
|
||||||
**Critical Persona Field Guidance Added:**
|
|
||||||
|
|
||||||
New documentation explaining how LLMs interpret persona fields for better agent quality:
|
|
||||||
|
|
||||||
- **role** → "What knowledge, skills, and capabilities do I possess?"
|
|
||||||
- **identity** → "What background, experience, and context shape my responses?"
|
|
||||||
- **communication_style** → "What verbal patterns, word choice, and phrasing do I use?"
|
|
||||||
- **principles** → "What beliefs and operating philosophy drive my choices?"
|
|
||||||
|
|
||||||
Key insight: `communication_style` should ONLY describe HOW the agent talks, not WHAT they do
|
|
||||||
|
|
||||||
**BMM Agent Voice Enhancement:**
|
|
||||||
|
|
||||||
All 9 existing BMM agents enhanced with distinct, memorable communication voices:
|
|
||||||
|
|
||||||
- **Mary (analyst)**: "Treats analysis like a treasure hunt - excited by every clue"
|
|
||||||
- **John (PM)**: "Asks 'WHY?' relentlessly like a detective on a case"
|
|
||||||
- **Winston (architect)**: "Champions boring technology that actually works"
|
|
||||||
- **Amelia (dev)**: "Ultra-succinct. Speaks in file paths and AC IDs"
|
|
||||||
- **Sally (UX)**: "Paints pictures with words, telling user stories that make you FEEL"
|
|
||||||
|
|
||||||
### 🔧 Edit-Agent Workflow Comprehensive Enhancement
|
|
||||||
|
|
||||||
**Expert Agent Sidecar Support (NEW):**
|
|
||||||
|
|
||||||
- Automatically detects and handles Expert agents with multiple files
|
|
||||||
- Loads and manages templates, data files, knowledge bases
|
|
||||||
- Smart sidecar analysis: maps references, finds orphans, validates paths
|
|
||||||
- 5 complete sidecar editing patterns with warm, educational feedback
|
|
||||||
|
|
||||||
**7-Step Communication Style Refinement Pattern:**
|
|
||||||
|
|
||||||
1. Diagnose current style with red flag word detection
|
|
||||||
2. Extract non-style content to working copy
|
|
||||||
3. Discover TRUE communication style through interview questions
|
|
||||||
4. Craft pure style using presets and reference agents
|
|
||||||
5. Show before/after transformation with full context
|
|
||||||
6. Validate against standards (zero red flags)
|
|
||||||
7. Confirm with user through dramatic reading
|
|
||||||
|
|
||||||
**Unified Validation Checklist:**
|
|
||||||
|
|
||||||
- Single source of truth: `agent-validation-checklist.md` (160 lines)
|
|
||||||
- Shared between create-agent and edit-agent workflows
|
|
||||||
- Comprehensive persona field separation validation
|
|
||||||
- Expert agent sidecar validation (9 specific checks)
|
|
||||||
- Common issues and fixes with real examples
|
|
||||||
|
|
||||||
### 📚 BMB Agent Builder Enhancement
|
### 📚 BMB Agent Builder Enhancement
|
||||||
|
|
||||||
**Vastly Improved Agent Creation & Editing Capabilities:**
|
- **Complete Documentation Suite**: 7 new guides for agent architecture and creation
|
||||||
|
- **Expert Agent Sidecar Support**: Multi-file agents with templates and knowledge bases
|
||||||
|
- **Unified Validation**: 160-line checklist shared across workflows
|
||||||
|
- **BMM Agent Voices**: All 9 agents enhanced with distinct communication styles
|
||||||
|
|
||||||
- Create-agent and edit-agent workflows now have accurate, comprehensive documentation
|
### 🎯 Workflow Architecture Change
|
||||||
- All context references updated and validated for consistency
|
|
||||||
- Workflows can now properly guide users through complex agent design decisions
|
|
||||||
|
|
||||||
**New Agent Documentation Suite:**
|
- **Epic Creation Moved**: Now in Phase 3 after Architecture for technical context
|
||||||
|
- **Excalidraw Distribution**: Diagram capabilities moved to role-appropriate agents
|
||||||
- `understanding-agent-types.md` - Architecture vs capability distinction
|
- **Google Antigravity IDE**: New installer with flattened file naming
|
||||||
- `simple-agent-architecture.md` - Self-contained agents guide
|
|
||||||
- `expert-agent-architecture.md` - Agents with sidecar files
|
|
||||||
- `module-agent-architecture.md` - Workflow-integrated agents
|
|
||||||
- `agent-compilation.md` - YAML → XML transformation process
|
|
||||||
- `agent-menu-patterns.md` - Menu design patterns
|
|
||||||
- `communication-presets.csv` - 60 pure communication styles for reference
|
|
||||||
|
|
||||||
**New Reference Agents for Learning:**
|
|
||||||
|
|
||||||
- Complete working examples of Simple, Expert, and Module agents
|
|
||||||
- Can be installed directly via the new `bmad agent-install` command
|
|
||||||
- Serve as both learning resources and ready-to-use agents
|
|
||||||
|
|
||||||
### 🎯 Epic Creation Moved to Phase 3 (After Architecture)
|
|
||||||
|
|
||||||
**Workflow Sequence Corrected:**
|
|
||||||
|
|
||||||
```
|
|
||||||
Phase 2: PRD → UX Design
|
|
||||||
Phase 3: Architecture → Epics & Stories ← NOW HERE (technically informed)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why This Fundamental Change:**
|
|
||||||
|
|
||||||
- Epics need architectural context: API contracts, data models, technical decisions
|
|
||||||
- Stories can reference actual architectural patterns and constraints
|
|
||||||
- Reduces rewrites when architecture reveals complexity
|
|
||||||
- Better complexity-based estimation (not time-based)
|
|
||||||
|
|
||||||
### 🖥️ New IDE Support
|
|
||||||
|
|
||||||
**Google Antigravity IDE Installer:**
|
|
||||||
|
|
||||||
- Flattened file naming for proper slash commands (bmad-module-agents-name.md)
|
|
||||||
- Namespace isolation prevents module conflicts
|
|
||||||
- Subagent installation support (project or user level)
|
|
||||||
- Module-specific injection configuration
|
|
||||||
|
|
||||||
**Codex CLI Enhancement:**
|
|
||||||
|
|
||||||
- Now supports both global and project-specific installation
|
|
||||||
- CODEX_HOME configuration for multi-project workflows
|
|
||||||
- OS-specific setup instructions (Unix/Mac/Windows)
|
|
||||||
|
|
||||||
### 🏗️ Reference Agents & Standards
|
|
||||||
|
|
||||||
**New Reference Agents Provide Clear Examples:**
|
|
||||||
|
|
||||||
- **commit-poet.agent.yaml**: Simple agent with pure communication style
|
|
||||||
- **journal-keeper.agent.yaml**: Expert agent with sidecar file structure
|
|
||||||
- **security-engineer.agent.yaml**: Module agent for ecosystem integration
|
|
||||||
- **trend-analyst.agent.yaml**: Module agent with cross-workflow capabilities
|
|
||||||
|
|
||||||
**Agent Type Clarification:**
|
|
||||||
|
|
||||||
- Clear documentation that agent types (Simple/Expert/Module) describe architecture, not capability
|
|
||||||
- Module = designed for ecosystem integration, not limited in function
|
|
||||||
|
|
||||||
### 🐛 Technical Improvements
|
|
||||||
|
|
||||||
**Linting Compliance:**
|
|
||||||
|
|
||||||
- Fixed all ESLint warnings across agent tooling
|
|
||||||
- `'utf-8'` → `'utf8'` (unicorn/text-encoding-identifier-case)
|
|
||||||
- `hasOwnProperty` → `Object.hasOwn` (unicorn/prefer-object-has-own)
|
|
||||||
- `JSON.parse(JSON.stringify(...))` → `structuredClone(...)`
|
|
||||||
|
|
||||||
**Agent Compilation Engine:**
|
|
||||||
|
|
||||||
- Auto-injects frontmatter, activation, handlers, help/exit menu items
|
|
||||||
- Smart handler inclusion (only includes handlers actually used)
|
|
||||||
- Proper XML escaping and formatting
|
|
||||||
- Persona name customization support
|
|
||||||
|
|
||||||
### 📊 Impact Summary
|
|
||||||
|
|
||||||
**New in alpha.11:**
|
|
||||||
|
|
||||||
- **Agent installation system** with `bmad agent-install` CLI command
|
|
||||||
- **4 new reference agents** (commit-poet, journal-keeper, security-engineer, trend-analyst)
|
|
||||||
- **Complete agent documentation suite** with 7 new focused guides
|
|
||||||
- **Expert agent sidecar support** in edit-agent workflow
|
|
||||||
- **2 new IDE installers** (Google Antigravity, enhanced Codex)
|
|
||||||
- **Unified validation checklist** (160 lines) for consistent quality standards
|
|
||||||
- **60 pure communication style presets** for agent persona design
|
|
||||||
|
|
||||||
**Enhanced from alpha.10:**
|
|
||||||
|
|
||||||
- **BMB agent builder workflows** with accurate context and comprehensive guidance
|
|
||||||
- **All 9 BMM agents** enhanced with distinct, memorable communication voices
|
|
||||||
- **Excalidraw capabilities** refined and distributed to role-appropriate agents
|
|
||||||
- **Epic creation** moved to Phase 3 (after Architecture) for technical context
|
|
||||||
|
|
||||||
### ⚠️ Breaking Changes
|
### ⚠️ Breaking Changes
|
||||||
|
|
||||||
**Agent Changes:**
|
1. **Frame Expert Retired**: Use role-appropriate agents for diagrams
|
||||||
|
2. **Agent Installation**: New bmad agent-install command replaces manual installation
|
||||||
- Frame Expert agent retired - diagram capabilities now available through role-appropriate agents:
|
3. **Epic Creation Phase**: Moved from Phase 2 to Phase 3
|
||||||
- Architecture diagrams → `/architect`
|
|
||||||
- Process flows → `/pm`
|
|
||||||
- Wireframes → `/ux-designer`
|
|
||||||
- Documentation visuals → `/tech-writer`
|
|
||||||
|
|
||||||
**Workflow Changes:**
|
|
||||||
|
|
||||||
- Epic creation moved from Phase 2 to Phase 3 (after Architecture)
|
|
||||||
- Excalidraw workflows redistributed to appropriate agents
|
|
||||||
|
|
||||||
**Installation Changes:**
|
|
||||||
|
|
||||||
- New `bmad agent-install` command replaces manual agent installation
|
|
||||||
- Agent YAML files must be compiled to XML for use
|
|
||||||
|
|
||||||
### 🔄 Migration Notes
|
|
||||||
|
|
||||||
**For Existing Projects:**
|
|
||||||
|
|
||||||
1. **Frame Expert Users:**
|
|
||||||
- Transition to role-appropriate agents for diagrams
|
|
||||||
- All Excalidraw functionality preserved and enhanced
|
|
||||||
- Shared templates now in core resources for wider access
|
|
||||||
|
|
||||||
2. **Agent Installation:**
|
|
||||||
- Use `bmad agent-install` for all agent installations
|
|
||||||
- Existing manual installations still work but won't have customization
|
|
||||||
|
|
||||||
3. **Epic Creation Timing:**
|
|
||||||
- Epics now created in Phase 3 after Architecture
|
|
||||||
- Update any automation expecting epics in Phase 2
|
|
||||||
|
|
||||||
4. **Communication Styles:**
|
|
||||||
- Review agent communication_style fields
|
|
||||||
- Remove any role/identity/principle content
|
|
||||||
- Use communication-presets.csv for pure styles
|
|
||||||
|
|
||||||
5. **Expert Agents:**
|
|
||||||
- Edit-agent workflow now fully supports sidecar files
|
|
||||||
- Organize templates and data files in agent folder
|
|
||||||
|
|
||||||
## [6.0.0-alpha.10]
|
## [6.0.0-alpha.10]
|
||||||
|
|
||||||
**Release: November 16, 2025**
|
**Release: November 16, 2025**
|
||||||
|
|
||||||
- **🎯 Epics Generated AFTER Architecture**: Major milestone - epics/stories now created after architecture for technically-informed user stories with better acceptance criteria
|
- **Epics After Architecture**: Major milestone - technically-informed user stories created post-architecture
|
||||||
- **🎨 Frame Expert Agent**: New Excalidraw specialist with 4 diagram workflows (flowchart, diagram, dataflow, wireframe) for visual documentation
|
- **Frame Expert Agent**: New Excalidraw specialist with 4 diagram workflows
|
||||||
- **⏰ Time Estimate Prohibition**: Critical warnings added across 33 workflows - acknowledges AI has fundamentally changed development speed
|
- **Time Estimate Prohibition**: Warnings across 33 workflows acknowledging AI's impact on development speed
|
||||||
- **🎯 Platform-Specific Commands**: New `ide-only`/`web-only` fields filter menu items based on environment (IDE vs web bundle)
|
- **Platform-Specific Commands**: ide-only/web-only fields filter menu items by environment
|
||||||
- **🔧 Agent Customization**: Enhanced memory/prompts merging via `*.customize.yaml` files for persistent agent personalization
|
- **Agent Customization**: Enhanced memory/prompts merging via \*.customize.yaml files
|
||||||
|
|
||||||
## [6.0.0-alpha.9]
|
## [6.0.0-alpha.9]
|
||||||
|
|
||||||
**Release: November 12, 2025**
|
**Release: November 12, 2025**
|
||||||
|
|
||||||
- **🚀 Intelligent File Discovery Protocol**: New `discover_inputs` with FULL_LOAD, SELECTIVE_LOAD, and INDEX_GUIDED strategies for automatic context loading
|
- **Intelligent File Discovery**: discover_inputs with FULL_LOAD, SELECTIVE_LOAD, INDEX_GUIDED strategies
|
||||||
- **📚 3-Track System**: Simplified from 5 levels to 3 intuitive tracks: quick-flow, bmad-method, and enterprise-bmad-method
|
- **3-Track System**: Simplified from 5 levels to 3 intuitive tracks
|
||||||
- **🌐 Web Bundles Guide**: Comprehensive documentation for Gemini Gems and Custom GPTs with 60-80% cost savings strategies
|
- **Web Bundles Guide**: Comprehensive documentation with 60-80% cost savings strategies
|
||||||
- **🏗️ Unified Output Structure**: Eliminated `.ephemeral/` folders - all artifacts now in single configurable output folder
|
- **Unified Output Structure**: Eliminated .ephemeral/ folders - single configurable output folder
|
||||||
- **🎮 BMGD Phase 4**: Added 10 game development workflows following BMM patterns with game-specific adaptations
|
- **BMGD Phase 4**: Added 10 game development workflows with BMM patterns
|
||||||
|
|
||||||
## [6.0.0-alpha.8]
|
## [6.0.0-alpha.8]
|
||||||
|
|
||||||
**Release: November 9, 2025**
|
**Release: November 9, 2025**
|
||||||
|
|
||||||
- **🎯 Configurable Installation**: Custom directories with `.bmad` hidden folder default for cleaner project structure
|
- **Configurable Installation**: Custom directories with .bmad hidden folder default
|
||||||
- **🚀 Optimized Agent Loading**: CLI loads from installed files eliminating duplication and maintenance burden
|
- **Optimized Agent Loading**: CLI loads from installed files, eliminating duplication
|
||||||
- **🌐 Party Mode Everywhere**: All web bundles include multi-agent collaboration with customizable party configurations
|
- **Party Mode Everywhere**: All web bundles include multi-agent collaboration
|
||||||
- **🔧 Phase 4 Artifact Separation**: Stories, code reviews, sprint plans now configurable outside docs folder
|
- **Phase 4 Artifact Separation**: Stories, code reviews, sprint plans configurable outside docs
|
||||||
- **📦 Expanded Web Bundles**: All BMM, BMGD, and CIS agents bundled with advanced elicitation integration
|
- **Expanded Web Bundles**: All BMM, BMGD, CIS agents bundled with elicitation integration
|
||||||
|
|
||||||
## [6.0.0-alpha.7]
|
## [6.0.0-alpha.7]
|
||||||
|
|
||||||
**Release: November 7, 2025**
|
**Release: November 7, 2025**
|
||||||
|
|
||||||
- **🌐 Workflow Vendoring**: Web bundler performs automatic workflow vendoring for cross-module dependencies
|
- **Workflow Vendoring**: Web bundler performs automatic cross-module dependency vendoring
|
||||||
- **🎮 BMGD Module Extraction**: Game development split into standalone module with 4-phase industry-standard structure
|
- **BMGD Module Extraction**: Game development split into standalone 4-phase structure
|
||||||
- **🔧 Enhanced Dependency Resolution**: Better handling of `web_bundle: false` workflows with positive resolution messages
|
- **Enhanced Dependency Resolution**: Better handling of web_bundle: false workflows
|
||||||
- **📚 Advanced Elicitation Fix**: Added missing CSV files to workflow bundles fixing runtime failures
|
- **Advanced Elicitation Fix**: Added missing CSV files to workflow bundles
|
||||||
- **🐛 Claude Code Fix**: Resolved README slash command installation regression
|
- **Claude Code Fix**: Resolved README slash command installation regression
|
||||||
|
|
||||||
## [6.0.0-alpha.6]
|
## [6.0.0-alpha.6]
|
||||||
|
|
||||||
**Release: November 4, 2025**
|
**Release: November 4, 2025**
|
||||||
|
|
||||||
- **🐛 Critical Installer Fixes**: Fixed manifestPath error and option display issues blocking installation
|
- **Critical Installer Fixes**: Fixed manifestPath error and option display issues
|
||||||
- **📖 Conditional Docs Installation**: Optional documentation installation to reduce footprint in production
|
- **Conditional Docs Installation**: Optional documentation to reduce production footprint
|
||||||
- **🎨 Improved Installer UX**: Better formatting with descriptive labels and clearer feedback
|
- **Improved Installer UX**: Better formatting with descriptive labels and clearer feedback
|
||||||
- **🧹 Issue Tracker Cleanup**: Closed 54 legacy v4 issues for focused v6 development
|
- **Issue Tracker Cleanup**: Closed 54 legacy v4 issues for focused v6 development
|
||||||
- **📝 Contributing Updates**: Removed references to non-existent branches in documentation
|
- **Contributing Updates**: Removed references to non-existent branches
|
||||||
|
|
||||||
## [6.0.0-alpha.5]
|
## [6.0.0-alpha.5]
|
||||||
|
|
||||||
**Release: November 4, 2025**
|
**Release: November 4, 2025**
|
||||||
|
|
||||||
- **🎯 3-Track Scale System**: Revolutionary simplification from 5 confusing levels to 3 intuitive preference-driven tracks
|
- **3-Track Scale System**: Simplified from 5 levels to 3 intuitive preference-driven tracks
|
||||||
- **✨ Elicitation Modernization**: Replaced legacy XML tags with explicit `invoke-task` pattern at strategic decision points
|
- **Elicitation Modernization**: Replaced legacy XML tags with explicit invoke-task pattern
|
||||||
- **📚 PM/UX Evolution Section**: Added November 2025 industry research on AI Agent PMs and Full-Stack Product Leads
|
- **PM/UX Evolution**: Added November 2025 industry research on AI Agent PMs
|
||||||
- **🏗️ Brownfield Reality Check**: Rewrote Phase 0 with 4 real-world scenarios for messy existing codebases
|
- **Brownfield Reality Check**: Rewrote Phase 0 with 4 real-world scenarios
|
||||||
- **📖 Documentation Accuracy**: All agent capabilities now match YAML source of truth with zero hallucination risk
|
- **Documentation Accuracy**: All agent capabilities now match YAML source of truth
|
||||||
|
|
||||||
## [6.0.0-alpha.4]
|
## [6.0.0-alpha.4]
|
||||||
|
|
||||||
**Release: November 2, 2025**
|
**Release: November 2, 2025**
|
||||||
|
|
||||||
- **📚 Documentation Hub**: Created 18 comprehensive guides (7000+ lines) with professional technical writing standards
|
- **Documentation Hub**: Created 18 comprehensive guides (7000+ lines) with professional standards
|
||||||
- **🤖 Paige Agent**: New technical documentation specialist available across all BMM phases
|
- **Paige Agent**: New technical documentation specialist across all BMM phases
|
||||||
- **🚀 Quick Spec Flow**: Intelligent Level 0-1 planning with auto-stack detection and brownfield analysis
|
- **Quick Spec Flow**: Intelligent Level 0-1 planning with auto-stack detection
|
||||||
- **📦 Universal Shard-Doc**: Split large markdown documents into organized sections with dual-strategy loading
|
- **Universal Shard-Doc**: Split large markdown documents with dual-strategy loading
|
||||||
- **🔧 Intent-Driven Planning**: PRD and Product Brief transformed from template-filling to natural conversation
|
- **Intent-Driven Planning**: PRD and Product Brief transformed from template-filling to conversation
|
||||||
|
|
||||||
## [6.0.0-alpha.3]
|
## [6.0.0-alpha.3]
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -19,7 +19,7 @@ A custom agents and workflows package follows this structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
my-custom-agents/
|
my-custom-agents/
|
||||||
├── custom.yaml # Package configuration
|
├── module.yaml # Package configuration
|
||||||
├── agents/ # Agent definitions
|
├── agents/ # Agent definitions
|
||||||
│ └── my-agent/
|
│ └── my-agent/
|
||||||
│ └── agent.md
|
│ └── agent.md
|
||||||
|
|
@ -30,7 +30,7 @@ my-custom-agents/
|
||||||
|
|
||||||
#### Configuration
|
#### Configuration
|
||||||
|
|
||||||
Create a `custom.yaml` file in your package root:
|
Create a `module.yaml` file in your package root:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
code: my-custom-agents
|
code: my-custom-agents
|
||||||
|
|
@ -42,11 +42,6 @@ default_selected: true
|
||||||
|
|
||||||
See `/example-custom-content` for a working example of a folder with multiple random custom agents and workflows. Technically its also just a module, but you will be able to further pick and choose from this folders contents of what you do and do not want to include in a destination folder. This way, you can store all custom content source in one location and easily install it to different locations.
|
See `/example-custom-content` for a working example of a folder with multiple random custom agents and workflows. Technically its also just a module, but you will be able to further pick and choose from this folders contents of what you do and do not want to include in a destination folder. This way, you can store all custom content source in one location and easily install it to different locations.
|
||||||
|
|
||||||
```bash
|
|
||||||
# The example is ready to use - just rename the config file:
|
|
||||||
mv example-custom-content/custom.bak example-custom-content/custom.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Custom Modules
|
### 2. Custom Modules
|
||||||
|
|
||||||
Custom modules are complete BMAD modules that can include their own configuration, documentation, along with agents and workflows that all compliment each other. Additionally they will have their own installation scripts, data, and potentially other tools. Modules can be used for:
|
Custom modules are complete BMAD modules that can include their own configuration, documentation, along with agents and workflows that all compliment each other. Additionally they will have their own installation scripts, data, and potentially other tools. Modules can be used for:
|
||||||
|
|
@ -64,7 +59,7 @@ A custom module follows this structure:
|
||||||
my-module/
|
my-module/
|
||||||
├── _module-installer/
|
├── _module-installer/
|
||||||
│ ├── installer.js # optional, when it exists it will run with module installation
|
│ ├── installer.js # optional, when it exists it will run with module installation
|
||||||
│ └── install-config.yaml # Module installation configuration with custom question and answer capture
|
├── module.yaml # Module installation configuration with custom question and answer capture
|
||||||
├── docs/ # Module documentation
|
├── docs/ # Module documentation
|
||||||
├── agents/ # Module-specific agents
|
├── agents/ # Module-specific agents
|
||||||
├── workflows/ # Module-specific workflows
|
├── workflows/ # Module-specific workflows
|
||||||
|
|
@ -77,7 +72,7 @@ my-module/
|
||||||
|
|
||||||
#### Module Configuration
|
#### Module Configuration
|
||||||
|
|
||||||
The `_module-installer/install-config.yaml` file defines how your module is installed:
|
The `module.yaml` file defines how your module is installed:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Module metadata
|
# Module metadata
|
||||||
|
|
@ -99,12 +94,6 @@ my_setting:
|
||||||
|
|
||||||
See `/example-custom-module` for a complete example:
|
See `/example-custom-module` for a complete example:
|
||||||
|
|
||||||
```bash
|
|
||||||
# The example is ready to use - just rename the _module-installer/install-config file:
|
|
||||||
mv example-custom-module/mwm/_module-installer/install-config.bak \
|
|
||||||
example-custom-module/mwm/_module-installer/install-config.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation Process
|
## Installation Process
|
||||||
|
|
||||||
### Step 1: Running the Installer
|
### Step 1: Running the Installer
|
||||||
|
|
@ -128,8 +117,7 @@ If you select "Enter a directory path", the installer will prompt for the locati
|
||||||
|
|
||||||
The installer will:
|
The installer will:
|
||||||
|
|
||||||
- Scan the directory and all subdirectories for the presence of a `custom.yaml` file (standalone content such as agents and workflows)
|
- Scan for `module.yaml` files (modules)
|
||||||
- Scan for `_module-installer/install-config.yaml` files (modules)
|
|
||||||
- Display an indication of how many installable folders it has found. Note that a project with stand along agents and workflows all under a single folder like the example will just list the count as 1 for that directory.
|
- Display an indication of how many installable folders it has found. Note that a project with stand along agents and workflows all under a single folder like the example will just list the count as 1 for that directory.
|
||||||
|
|
||||||
### Step 3: Selecting Content
|
### Step 3: Selecting Content
|
||||||
|
|
@ -230,7 +218,7 @@ Custom content can be distributed:
|
||||||
|
|
||||||
### No Custom Content Found
|
### No Custom Content Found
|
||||||
|
|
||||||
- Ensure your `custom.yaml` or `install-config.yaml` files are properly named
|
- Ensure your `module.yaml` files are properly named
|
||||||
- Check file permissions
|
- Check file permissions
|
||||||
- Verify the directory path is correct
|
- Verify the directory path is correct
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ project-root/
|
||||||
### Key Exclusions
|
### Key Exclusions
|
||||||
|
|
||||||
- `_module-installer/` directories are never copied to destination
|
- `_module-installer/` directories are never copied to destination
|
||||||
|
- module.yaml
|
||||||
- `localskip="true"` agents are filtered out
|
- `localskip="true"` agents are filtered out
|
||||||
- Source `config.yaml` templates are replaced with generated configs
|
- Source `config.yaml` templates are replaced with generated configs
|
||||||
|
|
||||||
|
|
@ -93,7 +94,7 @@ Creative Innovation Studio for design workflows
|
||||||
src/modules/{module}/
|
src/modules/{module}/
|
||||||
├── _module-installer/ # Not copied to destination
|
├── _module-installer/ # Not copied to destination
|
||||||
│ ├── installer.js # Post-install logic
|
│ ├── installer.js # Post-install logic
|
||||||
│ └── install-config.yaml
|
├── module.yaml
|
||||||
├── agents/
|
├── agents/
|
||||||
├── tasks/
|
├── tasks/
|
||||||
├── templates/
|
├── templates/
|
||||||
|
|
@ -107,7 +108,7 @@ src/modules/{module}/
|
||||||
|
|
||||||
### Collection Process
|
### Collection Process
|
||||||
|
|
||||||
Modules define prompts in `install-config.yaml`:
|
Modules define prompts in `module.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
project_name:
|
project_name:
|
||||||
|
|
@ -218,12 +219,12 @@ Platform-specific content without source modification:
|
||||||
src/modules/mymod/
|
src/modules/mymod/
|
||||||
├── _module-installer/
|
├── _module-installer/
|
||||||
│ ├── installer.js
|
│ ├── installer.js
|
||||||
│ └── install-config.yaml
|
├── module.yaml
|
||||||
├── agents/
|
├── agents/
|
||||||
└── tasks/
|
└── tasks/
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Configuration** (`install-config.yaml`)
|
2. **Configuration** (`module.yaml`)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
code: mymod
|
code: mymod
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
agent:
|
agent:
|
||||||
metadata:
|
metadata:
|
||||||
id: .bmad/agents/commit-poet/commit-poet.md
|
id: "{bmad_folder}/agents/commit-poet/commit-poet.md"
|
||||||
name: "Inkwell Von Comitizen"
|
name: "Inkwell Von Comitizen"
|
||||||
title: "Commit Message Artisan"
|
title: "Commit Message Artisan"
|
||||||
icon: "📜"
|
icon: "📜"
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ CLI uses Commander.js, commands auto-loaded from `tools/cli/commands/`:
|
||||||
### Core Architecture Patterns
|
### Core Architecture Patterns
|
||||||
|
|
||||||
1. **IDE Handlers**: Each IDE extends BaseIdeSetup class
|
1. **IDE Handlers**: Each IDE extends BaseIdeSetup class
|
||||||
2. **Module Installers**: Modules can have `_module-installer/installer.js`
|
2. **Module Installers**: Modules can have `module.yaml` and `_module-installer/installer.js`
|
||||||
3. **Sub-modules**: IDE-specific customizations in `sub-modules/{ide-name}/`
|
3. **Sub-modules**: IDE-specific customizations in `sub-modules/{ide-name}/`
|
||||||
4. **Shared Utilities**: `tools/cli/installers/lib/ide/shared/` contains generators
|
4. **Shared Utilities**: `tools/cli/installers/lib/ide/shared/` contains generators
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ Contains:
|
||||||
|
|
||||||
- Add new IDE handler: Create file in /tools/cli/installers/lib/ide/, extend BaseIdeSetup
|
- Add new IDE handler: Create file in /tools/cli/installers/lib/ide/, extend BaseIdeSetup
|
||||||
- Fix installer bug: Check installer.js (94KB - main logic)
|
- Fix installer bug: Check installer.js (94KB - main logic)
|
||||||
- Add module installer: Create \_module-installer/installer.js in module
|
- Add module installer: Create \_module-installer/installer.js if custom installer logic needed
|
||||||
- Update shared generators: Modify files in /shared/ directory
|
- Update shared generators: Modify files in /shared/ directory
|
||||||
|
|
||||||
## Relationships
|
## Relationships
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ src/modules/{module-name}/
|
||||||
│ ├── injections.yaml
|
│ ├── injections.yaml
|
||||||
│ ├── config.yaml
|
│ ├── config.yaml
|
||||||
│ └── sub-agents/
|
│ └── sub-agents/
|
||||||
├── install-config.yaml # Module install configuration
|
├── module.yaml # Module install configuration
|
||||||
└── README.md # Module documentation
|
└── README.md # Module documentation
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -145,7 +145,7 @@ Defined in @/tools/cli/lib/platform-codes.js
|
||||||
- Create new module installer: Add \_module-installer/installer.js
|
- Create new module installer: Add \_module-installer/installer.js
|
||||||
- Add IDE sub-module: Create sub-modules/{ide-name}/ with config
|
- Add IDE sub-module: Create sub-modules/{ide-name}/ with config
|
||||||
- Add new IDE support: Create handler in installers/lib/ide/
|
- Add new IDE support: Create handler in installers/lib/ide/
|
||||||
- Customize module installation: Modify install-config.yaml
|
- Customize module installation: Modify module.yaml
|
||||||
|
|
||||||
## Relationships
|
## Relationships
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
agent:
|
agent:
|
||||||
metadata:
|
metadata:
|
||||||
id: custom/agents/toolsmith/toolsmith.md
|
id: "{bmad_folder}/agents/toolsmith/toolsmith.md"
|
||||||
name: Vexor
|
name: Vexor
|
||||||
title: Infernal Toolsmith + Guardian of the BMAD Forge
|
title: Infernal Toolsmith + Guardian of the BMAD Forge
|
||||||
icon: ⚒️
|
icon: ⚒️
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
code: bmad-custom
|
code: bmad-custom
|
||||||
name: "BMAD-Custom: Sample Stand Alone Custom Agents and Workflows"
|
name: "BMAD-Custom: Sample Stand Alone Custom Agents and Workflows"
|
||||||
default_selected: true
|
default_selected: true
|
||||||
|
type: custom
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-01-init'
|
||||||
description: 'Initialize quiz game with mode selection and category choice'
|
description: 'Initialize quiz game with mode selection and category choice'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-01-init.md'
|
thisStepFile: '{workflow_path}/steps/step-01-init.md'
|
||||||
|
|
@ -66,7 +66,7 @@ To set up the quiz game by selecting game mode, choosing a category, and prepari
|
||||||
|
|
||||||
### 1. Welcome and Configuration Loading
|
### 1. Welcome and Configuration Loading
|
||||||
|
|
||||||
Load config from {project-root}/.bmad/bmb/config.yaml to get user_name.
|
Load config from {project-root}/{bmad_folder}/bmb/config.yaml to get user_name.
|
||||||
|
|
||||||
Present dramatic welcome:
|
Present dramatic welcome:
|
||||||
"🎺 _DRAMATIC MUSIC PLAYS_ 🎺
|
"🎺 _DRAMATIC MUSIC PLAYS_ 🎺
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-02-q1'
|
||||||
description: 'Question 1 - Level 1 difficulty'
|
description: 'Question 1 - Level 1 difficulty'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-02-q1.md'
|
thisStepFile: '{workflow_path}/steps/step-02-q1.md'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-03-q2'
|
||||||
description: 'Question 2 - Level 2 difficulty'
|
description: 'Question 2 - Level 2 difficulty'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-03-q2.md'
|
thisStepFile: '{workflow_path}/steps/step-03-q2.md'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-04-q3'
|
||||||
description: 'Question 3 - Level 3 difficulty'
|
description: 'Question 3 - Level 3 difficulty'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-04-q3.md'
|
thisStepFile: '{workflow_path}/steps/step-04-q3.md'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-05-q4'
|
||||||
description: 'Question 4 - Level 4 difficulty'
|
description: 'Question 4 - Level 4 difficulty'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-05-q4.md'
|
thisStepFile: '{workflow_path}/steps/step-05-q4.md'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-06-q5'
|
||||||
description: 'Question 5 - Level 5 difficulty'
|
description: 'Question 5 - Level 5 difficulty'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-06-q5.md'
|
thisStepFile: '{workflow_path}/steps/step-06-q5.md'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-07-q6'
|
||||||
description: 'Question 6 - Level 6 difficulty'
|
description: 'Question 6 - Level 6 difficulty'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-07-q6.md'
|
thisStepFile: '{workflow_path}/steps/step-07-q6.md'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-08-q7'
|
||||||
description: 'Question 7 - Level 7 difficulty'
|
description: 'Question 7 - Level 7 difficulty'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-08-q7.md'
|
thisStepFile: '{workflow_path}/steps/step-08-q7.md'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-09-q8'
|
||||||
description: 'Question 8 - Level 8 difficulty'
|
description: 'Question 8 - Level 8 difficulty'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-09-q8.md'
|
thisStepFile: '{workflow_path}/steps/step-09-q8.md'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-10-q9'
|
||||||
description: 'Question 9 - Level 9 difficulty'
|
description: 'Question 9 - Level 9 difficulty'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-10-q9.md'
|
thisStepFile: '{workflow_path}/steps/step-10-q9.md'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-11-q10'
|
||||||
description: 'Question 10 - Level 10 difficulty'
|
description: 'Question 10 - Level 10 difficulty'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-11-q10.md'
|
thisStepFile: '{workflow_path}/steps/step-11-q10.md'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ name: 'step-12-results'
|
||||||
description: 'Final results and celebration'
|
description: 'Final results and celebration'
|
||||||
|
|
||||||
# Path Definitions
|
# Path Definitions
|
||||||
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
|
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
|
||||||
|
|
||||||
# File References
|
# File References
|
||||||
thisStepFile: '{workflow_path}/steps/step-12-results.md'
|
thisStepFile: '{workflow_path}/steps/step-12-results.md'
|
||||||
|
|
|
||||||
|
|
@ -1,269 +0,0 @@
|
||||||
---
|
|
||||||
stepsCompleted: [1, 2, 3, 4, 5, 6, 7]
|
|
||||||
---
|
|
||||||
|
|
||||||
## Build Summary
|
|
||||||
|
|
||||||
**Date:** 2025-12-04
|
|
||||||
**Status:** Build Complete
|
|
||||||
|
|
||||||
### Files Generated
|
|
||||||
|
|
||||||
**Main Workflow:**
|
|
||||||
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/workflow.md`
|
|
||||||
|
|
||||||
**Step Files (12 total):**
|
|
||||||
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-01-init.md` - Game setup and mode selection
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-02-q1.md` - Question 1 (Level 1)
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-03-q2.md` - Question 2 (Level 2)
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-04-q3.md` - Question 3 (Level 3)
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-05-q4.md` - Question 4 (Level 4)
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-06-q5.md` - Question 5 (Level 5)
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-07-q6.md` - Question 6 (Level 6)
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-08-q7.md` - Question 7 (Level 7)
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-09-q8.md` - Question 8 (Level 8)
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-10-q9.md` - Question 9 (Level 9)
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-11-q10.md` - Question 10 (Level 10)
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-12-results.md` - Final results and celebration
|
|
||||||
|
|
||||||
**Templates:**
|
|
||||||
|
|
||||||
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/templates/csv-headers.template` - CSV column headers
|
|
||||||
|
|
||||||
### Key Features Implemented
|
|
||||||
|
|
||||||
1. **Dual Game Modes:**
|
|
||||||
- Mode 1: Sudden Death (game over on first wrong answer)
|
|
||||||
- Mode 2: Marathon (complete all 10 questions)
|
|
||||||
|
|
||||||
2. **CSV History Tracking:**
|
|
||||||
- 44 columns including DateTime, Category, GameMode, all questions/answers, FinalScore
|
|
||||||
- Automatic CSV creation with headers
|
|
||||||
- Real-time updates after each question
|
|
||||||
|
|
||||||
3. **Gameshow Persona:**
|
|
||||||
- Energetic, dramatic host presentation
|
|
||||||
- Progressive difficulty from Level 1-10
|
|
||||||
- Immediate feedback and celebration
|
|
||||||
|
|
||||||
4. **Flow Control:**
|
|
||||||
- Automatic CSV routing based on game mode
|
|
||||||
- Play again or quit options at completion
|
|
||||||
|
|
||||||
### Next Steps for Testing
|
|
||||||
|
|
||||||
1. Run the workflow: `/bmad:bmb:workflows:quiz-master`
|
|
||||||
2. Test both game modes
|
|
||||||
3. Verify CSV file creation and updates
|
|
||||||
4. Check question progression and difficulty
|
|
||||||
5. Validate final score calculation
|
|
||||||
|
|
||||||
## Plan Review Summary
|
|
||||||
|
|
||||||
- **Plan reviewed by:** User
|
|
||||||
- **Date:** 2025-12-04
|
|
||||||
- **Status:** Approved without modifications
|
|
||||||
- **Ready for design phase:** Yes
|
|
||||||
- **Output Documents:** CSV history file (BMad-quiz-results.csv)
|
|
||||||
|
|
||||||
# Workflow Creation Plan: quiz-master
|
|
||||||
|
|
||||||
## Initial Project Context
|
|
||||||
|
|
||||||
- **Module:** stand-alone
|
|
||||||
- **Target Location:** /Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master
|
|
||||||
- **Created:** 2025-12-04
|
|
||||||
|
|
||||||
## Detailed Requirements
|
|
||||||
|
|
||||||
### 1. Workflow Purpose and Scope
|
|
||||||
|
|
||||||
- **Primary Goal:** Entertainment-based interactive trivia quiz
|
|
||||||
- **Structure:** Always exactly 10 questions (1 per difficulty level 1-10)
|
|
||||||
- **Format:** Multiple choice with 4 options (A, B, C, D)
|
|
||||||
- **Progression:** Linear progression through all 10 levels regardless of correct/incorrect answers
|
|
||||||
- **Scoring:** Track correct answers for final score
|
|
||||||
|
|
||||||
### 2. Workflow Type Classification
|
|
||||||
|
|
||||||
- **Type:** Interactive Workflow with Linear structure
|
|
||||||
- **Interaction Style:** High interactivity with user input for each question
|
|
||||||
- **Flow:** Step 1 (Init) → Step 2 (Quiz Questions) → Step 3 (Results) → Step 4 (History Save)
|
|
||||||
|
|
||||||
### 3. Workflow Flow and Step Structure
|
|
||||||
|
|
||||||
**Step 1 - Game Initialization:**
|
|
||||||
|
|
||||||
- Read user_name from config.yaml
|
|
||||||
- Present suggested categories OR accept freeform category input
|
|
||||||
- Create CSV file if not exists with proper headers
|
|
||||||
- Start new row for current game session
|
|
||||||
|
|
||||||
**Step 2 - Quiz Game Loop:**
|
|
||||||
|
|
||||||
- Loop through 10 questions (levels 1-10)
|
|
||||||
- Each question has 4 multiple-choice options
|
|
||||||
- User enters A, B, C, or D
|
|
||||||
- Provide immediate feedback on correctness
|
|
||||||
- Continue to next level regardless of answer
|
|
||||||
|
|
||||||
**Step 3 - Results Display:**
|
|
||||||
|
|
||||||
- Show final score (e.g., "You got 7 out of 10!")
|
|
||||||
- Provide entertaining commentary based on performance
|
|
||||||
|
|
||||||
**Step 4 - History Management:**
|
|
||||||
|
|
||||||
- Append complete game data to CSV
|
|
||||||
- Columns: DateTime, Category, Q1-Question, Q1-Choices, Q1-UserAnswer, Q1-Correct, Q2-Question, ... Q10-Correct, FinalScore
|
|
||||||
|
|
||||||
### 4. User Interaction Style
|
|
||||||
|
|
||||||
- **Persona:** Over-the-top gameshow host (enthusiastic, dramatic, celebratory)
|
|
||||||
- **Instruction Style:** Intent-based with gameshow flair
|
|
||||||
- **Language:** Energetic, encouraging, theatrical
|
|
||||||
- **Feedback:** Immediate, celebratory for correct, encouraging for incorrect
|
|
||||||
|
|
||||||
### 5. Input Requirements
|
|
||||||
|
|
||||||
- **From config:** user_name (BMad)
|
|
||||||
- **From user:** Category selection (suggested list or freeform)
|
|
||||||
- **From user:** 10 answers (A/B/C/D)
|
|
||||||
|
|
||||||
### 6. Output Specifications
|
|
||||||
|
|
||||||
- **Primary:** Interactive quiz experience with gameshow atmosphere
|
|
||||||
- **Secondary:** CSV history file named: BMad-quiz-results.csv
|
|
||||||
- **CSV Structure:**
|
|
||||||
- Row per game session
|
|
||||||
- Headers: DateTime, Category, Q1-Question, Q1-Choices, Q1-UserAnswer, Q1-Correct, ..., Q10-Correct, FinalScore
|
|
||||||
|
|
||||||
### 7. Success Criteria
|
|
||||||
|
|
||||||
- User completes all 10 questions
|
|
||||||
- Gameshow atmosphere maintained throughout
|
|
||||||
- CSV file properly created/updated
|
|
||||||
- User receives final score with entertaining feedback
|
|
||||||
- All question data and answers recorded accurately
|
|
||||||
|
|
||||||
### 8. Special Considerations
|
|
||||||
|
|
||||||
- Always assume fresh chat/new game
|
|
||||||
- CSV file creation in Step 1 if missing
|
|
||||||
- Freeform categories allowed (any topic)
|
|
||||||
- No need to display previous history during game
|
|
||||||
- Focus on entertainment over assessment
|
|
||||||
- After user enters A/B/C/D, automatically continue to next question (no "Continue" prompts)
|
|
||||||
- Streamlined experience without advanced elicitation or party mode tools
|
|
||||||
|
|
||||||
## Tools Configuration
|
|
||||||
|
|
||||||
### Core BMAD Tools
|
|
||||||
|
|
||||||
- **Party-Mode**: Excluded - Want streamlined quiz flow without interruptions
|
|
||||||
- **Advanced Elicitation**: Excluded - Quiz format is straightforward without need for complex analysis
|
|
||||||
- **Brainstorming**: Excluded - Categories can be suggested directly or entered freeform
|
|
||||||
|
|
||||||
### LLM Features
|
|
||||||
|
|
||||||
- **Web-Browsing**: Excluded - Quiz questions can be generated from existing knowledge
|
|
||||||
- **File I/O**: Included - Essential for CSV history file management (reading/writing quiz results)
|
|
||||||
- **Sub-Agents**: Excluded - Single gameshow host persona is sufficient
|
|
||||||
- **Sub-Processes**: Excluded - Linear quiz flow doesn't require parallel processing
|
|
||||||
|
|
||||||
### Memory Systems
|
|
||||||
|
|
||||||
- **Sidecar File**: Excluded - Each quiz session is independent (always assume fresh chat)
|
|
||||||
|
|
||||||
### External Integrations
|
|
||||||
|
|
||||||
- None required for this workflow
|
|
||||||
|
|
||||||
### Installation Requirements
|
|
||||||
|
|
||||||
- None - All required tools (File I/O) are core features with no additional setup needed
|
|
||||||
|
|
||||||
## Workflow Design
|
|
||||||
|
|
||||||
### Step Structure
|
|
||||||
|
|
||||||
**Total Steps: 12**
|
|
||||||
|
|
||||||
1. Step 01 - Init: Mode selection, category choice, CSV setup
|
|
||||||
2. Steps 02-11: Individual questions (1-10) with CSV updates
|
|
||||||
3. Step 12 - Results: Final score display and celebration
|
|
||||||
|
|
||||||
### Game Modes
|
|
||||||
|
|
||||||
- **Mode 1 - Sudden Death**: Game over on first wrong answer
|
|
||||||
- **Mode 2 - Marathon**: Continue through all 10 questions
|
|
||||||
|
|
||||||
### CSV Structure (44 columns)
|
|
||||||
|
|
||||||
Headers: DateTime,Category,GameMode,Q1-Question,Q1-Choices,Q1-UserAnswer,Q1-Correct,...,Q10-Correct,FinalScore
|
|
||||||
|
|
||||||
### Flow Logic
|
|
||||||
|
|
||||||
- Step 01: Create row with DateTime, Category, GameMode
|
|
||||||
- Steps 02-11: Update CSV with question data
|
|
||||||
- Mode 1: IF incorrect → jump to Step 12
|
|
||||||
- Mode 2: Always continue
|
|
||||||
- Step 12: Update FinalScore, display results
|
|
||||||
|
|
||||||
### Gameshow Persona
|
|
||||||
|
|
||||||
- Energetic, dramatic host
|
|
||||||
- Celebratory feedback for correct answers
|
|
||||||
- Encouraging messages for incorrect
|
|
||||||
|
|
||||||
### File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
quiz-master/
|
|
||||||
├── workflow.md
|
|
||||||
├── steps/
|
|
||||||
│ ├── step-01-init.md
|
|
||||||
│ ├── step-02-q1.md
|
|
||||||
│ ├── ...
|
|
||||||
│ └── step-12-results.md
|
|
||||||
└── templates/
|
|
||||||
└── csv-headers.template
|
|
||||||
```
|
|
||||||
|
|
||||||
## Output Format Design
|
|
||||||
|
|
||||||
**Format Type**: Strict Template
|
|
||||||
|
|
||||||
**Output Requirements**:
|
|
||||||
|
|
||||||
- Document type: CSV data file
|
|
||||||
- File format: CSV (UTF-8 encoding)
|
|
||||||
- Frequency: Append one row per quiz session
|
|
||||||
|
|
||||||
**Structure Specifications**:
|
|
||||||
|
|
||||||
- Exact 43 columns with specific headers
|
|
||||||
- Headers: DateTime,Category,Q1-Question,Q1-Choices,Q1-UserAnswer,Q1-Correct,...,Q10-Correct,FinalScore
|
|
||||||
- Data formats:
|
|
||||||
- DateTime: ISO 8601 (YYYY-MM-DDTHH:MM:SS)
|
|
||||||
- Category: Text
|
|
||||||
- QX-Question: Text
|
|
||||||
- QX-Choices: (A)Opt1|(B)Opt2|(C)Opt3|(D)Opt4
|
|
||||||
- QX-UserAnswer: A/B/C/D
|
|
||||||
- QX-Correct: TRUE/FALSE
|
|
||||||
- FinalScore: Number (0-10)
|
|
||||||
|
|
||||||
**Template Information**:
|
|
||||||
|
|
||||||
- Template source: Created based on requirements
|
|
||||||
- Template file: CSV with fixed column structure
|
|
||||||
- Placeholders: None - strict format required
|
|
||||||
|
|
||||||
**Special Considerations**:
|
|
||||||
|
|
||||||
- CSV commas within text must be quoted
|
|
||||||
- Newlines in questions replaced with spaces
|
|
||||||
- Headers created only if file doesn't exist
|
|
||||||
- Append mode for all subsequent quiz sessions
|
|
||||||
|
|
@ -45,7 +45,7 @@ web_bundle: true
|
||||||
|
|
||||||
### 1. Module Configuration Loading
|
### 1. Module Configuration Loading
|
||||||
|
|
||||||
Load and read full config from {project-root}/.bmad/bmb/config.yaml and resolve:
|
Load and read full config from {project-root}/{bmad_folder}/bmb/config.yaml and resolve:
|
||||||
|
|
||||||
- `user_name`, `output_folder`, `communication_language`, `document_output_language`
|
- `user_name`, `output_folder`, `communication_language`, `document_output_language`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
This module is an example and is not at all recommended for any usage, this module was not vetted by any medical professionals and should
|
This module is an example and is not at all recommended for any usage, this module was not vetted by any medical professionals and should
|
||||||
be considered at best for entertainment purposes only.
|
be considered at best for entertainment purposes only.
|
||||||
|
|
||||||
IF you want to see how a custom module installation works, copy this whole folder to where you will be installing from with npx, and rename
|
|
||||||
"\_module-installer/install-config.bak" to "\_module-installer/install-config.yaml".
|
|
||||||
|
|
||||||
You should see the option in the module selector when installing.
|
You should see the option in the module selector when installing.
|
||||||
|
|
||||||
If you have received a module from someone else that is not in the official installation - you can install it similarly by running the
|
If you have received a module from someone else that is not in the official installation - you can install it similarly by running the
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
agent:
|
agent:
|
||||||
metadata:
|
metadata:
|
||||||
|
id: "{bmad_folder}/mwm/agents/cbt-coach/cbt-coach.md"
|
||||||
name: "Dr. Alexis, M.D."
|
name: "Dr. Alexis, M.D."
|
||||||
title: "CBT Coach"
|
title: "CBT Coach"
|
||||||
icon: "🧠"
|
icon: "🧠"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
agent:
|
agent:
|
||||||
metadata:
|
metadata:
|
||||||
|
id: "{bmad_folder}/mwm/agents/crisis-navigator.md"
|
||||||
name: "Beacon"
|
name: "Beacon"
|
||||||
title: "Crisis Navigator"
|
title: "Crisis Navigator"
|
||||||
icon: "🆘"
|
icon: "🆘"
|
||||||
|
|
@ -95,7 +96,7 @@ agent:
|
||||||
triggers:
|
triggers:
|
||||||
- trigger: party-mode
|
- trigger: party-mode
|
||||||
input: SPM or fuzzy match start party mode
|
input: SPM or fuzzy match start party mode
|
||||||
route: "{project-root}/.bmad/core/workflows/edit-agent/workflow.md"
|
route: "{project-root}/{bmad_folder}/core/workflows/edit-agent/workflow.md"
|
||||||
data: crisis navigator agent discussion
|
data: crisis navigator agent discussion
|
||||||
type: exec
|
type: exec
|
||||||
- trigger: expert-chat
|
- trigger: expert-chat
|
||||||
|
|
@ -117,7 +118,7 @@ agent:
|
||||||
type: action
|
type: action
|
||||||
|
|
||||||
- trigger: "safety-plan"
|
- trigger: "safety-plan"
|
||||||
route: "{project-root}/.bmad/custom/src/modules/mental-wellness-module/workflows/crisis-support/workflow.md"
|
route: "{project-root}/{bmad_folder}/custom/src/modules/mental-wellness-module/workflows/crisis-support/workflow.md"
|
||||||
description: "Create safety plan 🛡️"
|
description: "Create safety plan 🛡️"
|
||||||
type: workflow
|
type: workflow
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
agent:
|
agent:
|
||||||
metadata:
|
metadata:
|
||||||
|
id: "{bmad_folder}/mwm/agents/meditation-guide.md"
|
||||||
name: "Serenity"
|
name: "Serenity"
|
||||||
title: "Meditation Guide"
|
title: "Meditation Guide"
|
||||||
icon: "🧘"
|
icon: "🧘"
|
||||||
|
|
@ -92,7 +93,7 @@ agent:
|
||||||
triggers:
|
triggers:
|
||||||
- trigger: party-mode
|
- trigger: party-mode
|
||||||
input: SPM or fuzzy match start party mode
|
input: SPM or fuzzy match start party mode
|
||||||
route: "{project-root}/.bmad/core/workflows/edit-agent/workflow.md"
|
route: "{project-root}/{bmad_folder}/core/workflows/edit-agent/workflow.md"
|
||||||
data: meditation guide agent discussion
|
data: meditation guide agent discussion
|
||||||
type: exec
|
type: exec
|
||||||
- trigger: expert-chat
|
- trigger: expert-chat
|
||||||
|
|
@ -104,7 +105,7 @@ agent:
|
||||||
triggers:
|
triggers:
|
||||||
- trigger: guided-meditation
|
- trigger: guided-meditation
|
||||||
input: GM or fuzzy match guided meditation
|
input: GM or fuzzy match guided meditation
|
||||||
route: "{project-root}/.bmad/custom/src/modules/mental-wellness-module/workflows/guided-meditation/workflow.md"
|
route: "{project-root}/{bmad_folder}/custom/src/modules/mental-wellness-module/workflows/guided-meditation/workflow.md"
|
||||||
description: "Full meditation session 🧘"
|
description: "Full meditation session 🧘"
|
||||||
type: workflow
|
type: workflow
|
||||||
- trigger: body-scan
|
- trigger: body-scan
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
agent:
|
agent:
|
||||||
metadata:
|
metadata:
|
||||||
|
id: "{bmad_folder}/mwm/agents/wellness-companion/wellness-companion.md"
|
||||||
name: "Riley"
|
name: "Riley"
|
||||||
title: "Wellness Companion"
|
title: "Wellness Companion"
|
||||||
icon: "🌱"
|
icon: "🌱"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
code: mwm
|
code: mwm
|
||||||
name: "MWM: Mental Wellness Module"
|
name: "MWM: Mental Wellness Module"
|
||||||
default_selected: false
|
default_selected: false
|
||||||
|
type: module
|
||||||
|
|
||||||
header: "MWM™: Custom Wellness Module"
|
header: "MWM™: Custom Wellness Module"
|
||||||
subheader: "Demo of Potential Non Coding Custom Module Use case"
|
subheader: "Demo of Potential Non Coding Custom Module Use case"
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "bmad-method",
|
"name": "bmad-method",
|
||||||
"version": "6.0.0-alpha.14",
|
"version": "6.0.0-alpha.15",
|
||||||
"description": "Breakthrough Method of Agile AI-driven Development",
|
"description": "Breakthrough Method of Agile AI-driven Development",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"agile",
|
"agile",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const chalk = require('chalk');
|
||||||
*
|
*
|
||||||
* @param {Object} options - Installation options
|
* @param {Object} options - Installation options
|
||||||
* @param {string} options.projectRoot - The root directory of the target project
|
* @param {string} options.projectRoot - The root directory of the target project
|
||||||
* @param {Object} options.config - Module configuration from install-config.yaml
|
* @param {Object} options.config - Module configuration from module.yaml
|
||||||
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
||||||
* @param {Object} options.logger - Logger instance for output
|
* @param {Object} options.logger - Logger instance for output
|
||||||
* @returns {Promise<boolean>} - Success status
|
* @returns {Promise<boolean>} - Success status
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,14 @@
|
||||||
|
|
||||||
<format>
|
<format>
|
||||||
**Advanced Elicitation Options (If you launched Party Mode, they will participate randomly)**
|
**Advanced Elicitation Options (If you launched Party Mode, they will participate randomly)**
|
||||||
Choose a number (1-5), [r] to Reshuffle, [a] List All, or [x] to Proceed:
|
Choose a number (1-5), [r] Random 5, [a] List All, or [x] to Proceed:
|
||||||
|
|
||||||
1. [Method Name]
|
1. [Method Name]
|
||||||
2. [Method Name]
|
2. [Method Name]
|
||||||
3. [Method Name]
|
3. [Method Name]
|
||||||
4. [Method Name]
|
4. [Method Name]
|
||||||
5. [Method Name]
|
5. [Method Name]
|
||||||
r. Reshuffle the list with 5 new options
|
r. Pick 5 random methods
|
||||||
a. List all methods with descriptions
|
a. List all methods with descriptions
|
||||||
x. Proceed / No Further Actions
|
x. Proceed / No Further Actions
|
||||||
</format>
|
</format>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const chalk = require('chalk');
|
||||||
*
|
*
|
||||||
* @param {Object} options - Installation options
|
* @param {Object} options - Installation options
|
||||||
* @param {string} options.projectRoot - The root directory of the target project
|
* @param {string} options.projectRoot - The root directory of the target project
|
||||||
* @param {Object} options.config - Module configuration from install-config.yaml
|
* @param {Object} options.config - Module configuration from module.yaml
|
||||||
* @param {Object} options.coreConfig - Core configuration containing user_name
|
* @param {Object} options.coreConfig - Core configuration containing user_name
|
||||||
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
||||||
* @param {Object} options.logger - Logger instance for output
|
* @param {Object} options.logger - Logger instance for output
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,8 @@ For a [module type] module, we'll create this structure:"
|
||||||
│ └── [template-files]
|
│ └── [template-files]
|
||||||
├── data/ # Module data files
|
├── data/ # Module data files
|
||||||
│ └── [data-files]
|
│ └── [data-files]
|
||||||
|
├── module.yaml # Required
|
||||||
├── _module-installer/ # Installation configuration
|
├── _module-installer/ # Installation configuration
|
||||||
│ ├── install-config.yaml # Required
|
|
||||||
│ ├── installer.js # Optional
|
│ ├── installer.js # Optional
|
||||||
│ └── assets/ # Optional install assets
|
│ └── assets/ # Optional install assets
|
||||||
└── README.md # Module documentation
|
└── README.md # Module documentation
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ Update module-plan.md with configuration section:
|
||||||
|
|
||||||
### Result Configuration Structure
|
### Result Configuration Structure
|
||||||
|
|
||||||
The install-config.yaml will generate:
|
The module.yaml will generate:
|
||||||
- Module configuration at: {bmad_folder}/{module_code}/config.yaml
|
- Module configuration at: {bmad_folder}/{module_code}/config.yaml
|
||||||
- User settings stored as: [describe structure]
|
- User settings stored as: [describe structure]
|
||||||
````
|
````
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ partyModeWorkflow: '{project-root}/{bmad_folder}/core/workflows/party-mode/workf
|
||||||
## EXECUTION PROTOCOLS:
|
## EXECUTION PROTOCOLS:
|
||||||
|
|
||||||
- 🎯 Use configuration plan from step 5
|
- 🎯 Use configuration plan from step 5
|
||||||
- 💾 Create install-config.yaml with all fields
|
- 💾 Create module.yaml with all fields
|
||||||
- 📖 Add "step-08-installer" to stepsCompleted array` before loading next step
|
- 📖 Add "step-08-installer" to stepsCompleted array` before loading next step
|
||||||
- 🚫 FORBIDDEN to load next step until user selects 'C'
|
- 🚫 FORBIDDEN to load next step until user selects 'C'
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@ partyModeWorkflow: '{project-root}/{bmad_folder}/core/workflows/party-mode/workf
|
||||||
|
|
||||||
## STEP GOAL:
|
## STEP GOAL:
|
||||||
|
|
||||||
To create the module installer configuration (install-config.yaml) that defines how users will install and configure the module.
|
To create the module installer configuration (module.yaml) that defines how users will install and configure the module.
|
||||||
|
|
||||||
## INSTALLER SETUP PROCESS:
|
## INSTALLER SETUP PROCESS:
|
||||||
|
|
||||||
|
|
@ -74,11 +74,11 @@ From step 5, we planned these configuration fields:
|
||||||
Ensure \_module-installer directory exists
|
Ensure \_module-installer directory exists
|
||||||
Directory: {custom_module_location}/{module_name}/\_module-installer/
|
Directory: {custom_module_location}/{module_name}/\_module-installer/
|
||||||
|
|
||||||
### 3. Create install-config.yaml
|
### 3. Create module.yaml
|
||||||
|
|
||||||
"I'll create the install-config.yaml file based on your configuration plan. This is the core installer configuration file."
|
"I'll create the module.yaml file based on your configuration plan. This is the core installer configuration file."
|
||||||
|
|
||||||
Create file: {custom_module_location}/{module_name}/\_module-installer/install-config.yaml from template {installConfigTemplate}
|
Create file: {custom_module_location}/{module_name}/module.yaml from template {installConfigTemplate}
|
||||||
|
|
||||||
### 4. Handle Custom Installation Logic
|
### 4. Handle Custom Installation Logic
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ Update module-plan.md with installer section:
|
||||||
|
|
||||||
### Install Configuration
|
### Install Configuration
|
||||||
|
|
||||||
- File: \_module-installer/install-config.yaml
|
- File: module.yaml
|
||||||
- Module code: {module_name}
|
- Module code: {module_name}
|
||||||
- Default selected: false
|
- Default selected: false
|
||||||
- Configuration fields: [count]
|
- Configuration fields: [count]
|
||||||
|
|
@ -166,7 +166,7 @@ Display: **Select an Option:** [A] Advanced Elicitation [P] Party Mode [C] Conti
|
||||||
|
|
||||||
### ✅ SUCCESS:
|
### ✅ SUCCESS:
|
||||||
|
|
||||||
- install-config.yaml created with all planned fields
|
- module.yaml created with all planned fields
|
||||||
- YAML syntax valid
|
- YAML syntax valid
|
||||||
- Custom installation logic prepared (if needed)
|
- Custom installation logic prepared (if needed)
|
||||||
- Installer follows BMAD standards
|
- Installer follows BMAD standards
|
||||||
|
|
@ -174,7 +174,7 @@ Display: **Select an Option:** [A] Advanced Elicitation [P] Party Mode [C] Conti
|
||||||
|
|
||||||
### ❌ SYSTEM FAILURE:
|
### ❌ SYSTEM FAILURE:
|
||||||
|
|
||||||
- Not creating install-config.yaml
|
- Not creating module.yaml
|
||||||
- Invalid YAML syntax
|
- Invalid YAML syntax
|
||||||
- Missing required fields
|
- Missing required fields
|
||||||
- Not using proper path templates
|
- Not using proper path templates
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,8 @@ bmad install {module_name}
|
||||||
├── tasks/ # Task files
|
├── tasks/ # Task files
|
||||||
├── templates/ # Shared templates
|
├── templates/ # Shared templates
|
||||||
├── data/ # Module data
|
├── data/ # Module data
|
||||||
├── _module-installer/ # Installation config
|
├── _module-installer/ # Installation optional js file with custom install routine
|
||||||
|
├── module.yaml # yaml config and install questions
|
||||||
└── README.md # This file
|
└── README.md # This file
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ workflow {workflow_name}
|
||||||
├── data/ # ✅ Created
|
├── data/ # ✅ Created
|
||||||
├── _module-installer/ # ✅ Configured
|
├── _module-installer/ # ✅ Configured
|
||||||
└── README.md # ✅ Complete
|
└── README.md # ✅ Complete
|
||||||
|
└── module.yaml # ✅ Complete
|
||||||
```
|
```
|
||||||
|
|
||||||
## Completion Criteria
|
## Completion Criteria
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,8 @@ Expected Structure:
|
||||||
├── templates/ [✅/❌]
|
├── templates/ [✅/❌]
|
||||||
├── data/ [✅/❌]
|
├── data/ [✅/❌]
|
||||||
├── _module-installer/ [✅/❌]
|
├── _module-installer/ [✅/❌]
|
||||||
│ ├── install-config.yaml [✅/❌]
|
|
||||||
│ └── installer.js [✅/N/A]
|
│ └── installer.js [✅/N/A]
|
||||||
|
├── module.yaml [✅/❌]
|
||||||
└── README.md [✅/❌]
|
└── README.md [✅/❌]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -87,7 +87,7 @@ Expected Structure:
|
||||||
"**2. Configuration Files Check**"
|
"**2. Configuration Files Check**"
|
||||||
|
|
||||||
**Install Configuration:**
|
**Install Configuration:**
|
||||||
Validate install-config.yaml
|
Validate module.yaml
|
||||||
|
|
||||||
- [ ] YAML syntax valid
|
- [ ] YAML syntax valid
|
||||||
- [ ] Module code matches folder name
|
- [ ] Module code matches folder name
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
/**
|
/**
|
||||||
* @param {Object} options - Installation options
|
* @param {Object} options - Installation options
|
||||||
* @param {string} options.projectRoot - Project root directory
|
* @param {string} options.projectRoot - Project root directory
|
||||||
* @param {Object} options.config - Module configuration from install-config.yaml
|
* @param {Object} options.config - Module configuration from module.yaml
|
||||||
* @param {Array} options.installedIDEs - List of IDE codes being configured
|
* @param {Array} options.installedIDEs - List of IDE codes being configured
|
||||||
* @param {Object} options.logger - Logger instance (log, warn, error methods)
|
* @param {Object} options.logger - Logger instance (log, warn, error methods)
|
||||||
* @returns {boolean} - true if successful, false to abort installation
|
* @returns {boolean} - true if successful, false to abort installation
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,15 @@ This document provides the validation criteria used in step-11-validate.md to en
|
||||||
- [ ] data/ - Module data
|
- [ ] data/ - Module data
|
||||||
- [ ] \_module-installer/ - Installation config
|
- [ ] \_module-installer/ - Installation config
|
||||||
- [ ] README.md - Module documentation
|
- [ ] README.md - Module documentation
|
||||||
|
- [ ] module.yaml - module config file
|
||||||
|
|
||||||
### Required Files in \_module-installer/
|
### Optional File in \_module-installer/
|
||||||
|
|
||||||
- [ ] install-config.yaml - Installation configuration
|
|
||||||
- [ ] installer.js - Custom logic (if needed)
|
- [ ] installer.js - Custom logic (if needed)
|
||||||
|
|
||||||
## Configuration Validation
|
## Configuration Validation
|
||||||
|
|
||||||
### install-config.yaml
|
### module.yaml
|
||||||
|
|
||||||
- [ ] Valid YAML syntax
|
- [ ] Valid YAML syntax
|
||||||
- [ ] Module code matches folder name
|
- [ ] Module code matches folder name
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ After getting the workflow name:
|
||||||
Based on the module selection, confirm the target location:
|
Based on the module selection, confirm the target location:
|
||||||
|
|
||||||
- For bmb module: `{custom_workflow_location}` (defaults to `{bmad_folder}/custom/src/workflows`)
|
- For bmb module: `{custom_workflow_location}` (defaults to `{bmad_folder}/custom/src/workflows`)
|
||||||
- For other modules: Check their install-config.yaml for custom workflow locations
|
- For other modules: Check their module.yaml for custom workflow locations
|
||||||
- Confirm the exact folder path where the workflow will be created
|
- Confirm the exact folder path where the workflow will be created
|
||||||
- Store the confirmed path as `{targetWorkflowPath}`
|
- Store the confirmed path as `{targetWorkflowPath}`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ Create the workflow folder structure in the target location:
|
||||||
```
|
```
|
||||||
|
|
||||||
For bmb module, this will be: `{bmad_folder}/custom/src/workflows/{workflow_name}/`
|
For bmb module, this will be: `{bmad_folder}/custom/src/workflows/{workflow_name}/`
|
||||||
For other modules, check their install-config.yaml for custom_workflow_location
|
For other modules, check their module.yaml for custom_workflow_location
|
||||||
|
|
||||||
### 3. Generate workflow.md
|
### 3. Generate workflow.md
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,8 +129,9 @@ bmgd/
|
||||||
│ (Uses BMM workflows via cross-module references)
|
│ (Uses BMM workflows via cross-module references)
|
||||||
├── templates/
|
├── templates/
|
||||||
├── data/
|
├── data/
|
||||||
|
├── module.yaml
|
||||||
└── _module-installer/
|
└── _module-installer/
|
||||||
└── install-config.yaml
|
└── installer.js (optional)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const platformCodes = require(path.join(__dirname, '../../../../tools/cli/lib/pl
|
||||||
*
|
*
|
||||||
* @param {Object} options - Installation options
|
* @param {Object} options - Installation options
|
||||||
* @param {string} options.projectRoot - The root directory of the target project
|
* @param {string} options.projectRoot - The root directory of the target project
|
||||||
* @param {Object} options.config - Module configuration from install-config.yaml
|
* @param {Object} options.config - Module configuration from module.yaml
|
||||||
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
||||||
* @param {Object} options.logger - Logger instance for output
|
* @param {Object} options.logger - Logger instance for output
|
||||||
* @returns {Promise<boolean>} - Success status
|
* @returns {Promise<boolean>} - Success status
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const chalk = require('chalk');
|
||||||
*
|
*
|
||||||
* @param {Object} options - Installation options
|
* @param {Object} options - Installation options
|
||||||
* @param {string} options.projectRoot - The root directory of the target project
|
* @param {string} options.projectRoot - The root directory of the target project
|
||||||
* @param {Object} options.config - Module configuration from install-config.yaml
|
* @param {Object} options.config - Module configuration from module.yaml
|
||||||
* @param {Object} options.logger - Logger instance for output
|
* @param {Object} options.logger - Logger instance for output
|
||||||
* @param {Object} options.platformInfo - Platform metadata from global config
|
* @param {Object} options.platformInfo - Platform metadata from global config
|
||||||
* @returns {Promise<boolean>} - Success status
|
* @returns {Promise<boolean>} - Success status
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const chalk = require('chalk');
|
||||||
*
|
*
|
||||||
* @param {Object} options - Installation options
|
* @param {Object} options - Installation options
|
||||||
* @param {string} options.projectRoot - The root directory of the target project
|
* @param {string} options.projectRoot - The root directory of the target project
|
||||||
* @param {Object} options.config - Module configuration from install-config.yaml
|
* @param {Object} options.config - Module configuration from module.yaml
|
||||||
* @param {Object} options.logger - Logger instance for output
|
* @param {Object} options.logger - Logger instance for output
|
||||||
* @returns {Promise<boolean>} - Success status
|
* @returns {Promise<boolean>} - Success status
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -139,9 +139,6 @@ Comprehensive documentation for all BMM workflows organized by phase:
|
||||||
- Complete story lifecycle
|
- Complete story lifecycle
|
||||||
- One-story-at-a-time discipline
|
- One-story-at-a-time discipline
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
|
|
||||||
- **[Testing & QA Workflows](./test-architecture.md)** - Comprehensive quality assurance (1,420 lines)
|
- **[Testing & QA Workflows](./test-architecture.md)** - Comprehensive quality assurance (1,420 lines)
|
||||||
- Test strategy, automation, quality gates
|
- Test strategy, automation, quality gates
|
||||||
- TEA agent and test healing
|
- TEA agent and test healing
|
||||||
|
|
@ -149,14 +146,6 @@ Comprehensive documentation for all BMM workflows organized by phase:
|
||||||
|
|
||||||
**Total: 34 workflows documented across all phases**
|
**Total: 34 workflows documented across all phases**
|
||||||
|
|
||||||
=======
|
|
||||||
|
|
||||||
> > > > > > > Stashed changes
|
|
||||||
|
|
||||||
=======
|
|
||||||
|
|
||||||
> > > > > > > Stashed changes
|
|
||||||
|
|
||||||
### Advanced Workflow References
|
### Advanced Workflow References
|
||||||
|
|
||||||
For detailed technical documentation on specific complex workflows:
|
For detailed technical documentation on specific complex workflows:
|
||||||
|
|
@ -181,23 +170,9 @@ Quality assurance guidance:
|
||||||
|
|
||||||
<!-- Test Architect documentation to be added -->
|
<!-- Test Architect documentation to be added -->
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
|
|
||||||
- Test design workflows
|
- Test design workflows
|
||||||
- Quality gates
|
- Quality gates
|
||||||
- Risk assessment
|
- Risk assessment
|
||||||
- # NFR validation
|
|
||||||
=======
|
|
||||||
> > > > > > > Stashed changes
|
|
||||||
- Test design workflows
|
|
||||||
- Quality gates
|
|
||||||
- Risk assessment
|
|
||||||
- NFR validation
|
|
||||||
> > > > > > > Stashed changes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Module Structure
|
## 🏗️ Module Structure
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,6 @@ The `sprint-status.yaml` file is the single source of truth for all implementati
|
||||||
### (BMad Method / Enterprise)
|
### (BMad Method / Enterprise)
|
||||||
|
|
||||||
```
|
```
|
||||||
<<<<<<< Updated upstream
|
|
||||||
PRD (PM) → Architecture (Architect)
|
PRD (PM) → Architecture (Architect)
|
||||||
→ create-epics-and-stories (PM) ← V6: After architecture!
|
→ create-epics-and-stories (PM) ← V6: After architecture!
|
||||||
→ implementation-readiness (Architect)
|
→ implementation-readiness (Architect)
|
||||||
|
|
@ -142,7 +141,6 @@ PRD (PM) → Architecture (Architect)
|
||||||
→ story loop (SM/DEV)
|
→ story loop (SM/DEV)
|
||||||
→ retrospective (SM)
|
→ retrospective (SM)
|
||||||
→ [Next Epic]
|
→ [Next Epic]
|
||||||
=======
|
|
||||||
Current Phase: 4 (Implementation)
|
Current Phase: 4 (Implementation)
|
||||||
Current Epic: Epic 1 (Authentication)
|
Current Epic: Epic 1 (Authentication)
|
||||||
Current Sprint: Sprint 1
|
Current Sprint: Sprint 1
|
||||||
|
|
@ -190,108 +188,12 @@ See: [workflow-status instructions](../workflows/workflow-status/instructions.md
|
||||||
|
|
||||||
See: [document-project reference](./workflow-document-project-reference.md)
|
See: [document-project reference](./workflow-document-project-reference.md)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Story Lifecycle Visualization
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ PHASE 4: IMPLEMENTATION (Iterative Story Lifecycle) │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Sprint Planning │ → Creates sprint-status.yaml
|
|
||||||
└────────┬────────┘ Defines story queue
|
|
||||||
│
|
|
||||||
├──────────────────────────────────────────┐
|
|
||||||
│ │
|
|
||||||
▼ │
|
|
||||||
┌─────────────────────┐ │
|
|
||||||
│ Epic Tech Context │ → Optional per epic │
|
|
||||||
│ (Once per epic) │ Provides technical │
|
|
||||||
└─────────────────────┘ guidance │
|
|
||||||
│ │
|
|
||||||
▼ │
|
|
||||||
┌─────────────────────────────────────────────────┤
|
|
||||||
│ FOR EACH STORY IN QUEUE: │
|
|
||||||
├─────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
▼ │
|
|
||||||
┌─────────────────┐ │
|
|
||||||
│ Create Story │ → Generates story file │
|
|
||||||
│ (TODO → IN PROGRESS) │
|
|
||||||
└────────┬────────┘ │
|
|
||||||
│ │
|
|
||||||
▼ │
|
|
||||||
┌─────────────────┐ │
|
|
||||||
│ Story Context │ → Assembles focused context │
|
|
||||||
└────────┬────────┘ │
|
|
||||||
│ │
|
|
||||||
▼ │
|
|
||||||
┌─────────────────┐ │
|
|
||||||
│ Dev Story │ → Implements + tests │
|
|
||||||
│ (IN PROGRESS) │ │
|
|
||||||
└────────┬────────┘ │
|
|
||||||
│ │
|
|
||||||
▼ │
|
|
||||||
┌─────────────────┐ │
|
|
||||||
│ Code Review │ → Senior dev review │
|
|
||||||
│ (IN PROGRESS → │ │
|
|
||||||
│ READY FOR REVIEW) │
|
|
||||||
└────────┬────────┘ │
|
|
||||||
│ │
|
|
||||||
┌────┴────┐ │
|
|
||||||
│ Result? │ │
|
|
||||||
└────┬────┘ │
|
|
||||||
│ │
|
|
||||||
┌────┼────────────────────┐ │
|
|
||||||
│ │ │ │
|
|
||||||
▼ ▼ ▼ │
|
|
||||||
APPROVED APPROVED REQUEST │
|
|
||||||
WITH COMMENTS CHANGES │
|
|
||||||
│ │ │ │
|
|
||||||
└─────────┴───────────────────┘ │
|
|
||||||
│ │
|
|
||||||
▼ │
|
|
||||||
┌─────────────────┐ │
|
|
||||||
│ Story Done │ → READY FOR REVIEW → DONE│
|
|
||||||
└────────┬────────┘ │
|
|
||||||
│ │
|
|
||||||
├─────────────────────────────────────┘
|
|
||||||
│ More stories?
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌────────────────┐
|
|
||||||
│ Epic Complete? │
|
|
||||||
└────────┬───────┘
|
|
||||||
│
|
|
||||||
┌────┼────┐
|
|
||||||
│ │
|
|
||||||
Yes No
|
|
||||||
│ └──> Continue to next story
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Retrospective │ → Review epic, lessons learned
|
|
||||||
└─────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
All epics done?
|
|
||||||
│
|
|
||||||
Yes → PROJECT COMPLETE
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Phase 1: Analysis Workflows](./workflows-analysis.md)
|
- [Phase 1: Analysis Workflows](./workflows-analysis.md)
|
||||||
- [Phase 2: Planning Workflows](./workflows-planning.md)
|
- [Phase 2: Planning Workflows](./workflows-planning.md)
|
||||||
- [Phase 3: Solutioning Workflows](./workflows-solutioning.md)
|
- [Phase 3: Solutioning Workflows](./workflows-solutioning.md)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**Q: Which workflow should I run next?**
|
**Q: Which workflow should I run next?**
|
||||||
|
|
@ -306,6 +208,4 @@ A: Not recommended. Complete one story's full lifecycle before starting the next
|
||||||
**Q: What if code review finds issues?**
|
**Q: What if code review finds issues?**
|
||||||
A: DEV runs `dev-story` to make fixes, re-runs tests, then runs `code-review` again until it passes.
|
A: DEV runs `dev-story` to make fixes, re-runs tests, then runs `code-review` again until it passes.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
_Phase 4 Implementation - One story at a time, done right._
|
_Phase 4 Implementation - One story at a time, done right._
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
code: bmm
|
code: bmm
|
||||||
name: "BMM: BMad Method Agile-AI Driven-Development"
|
name: "BMM: BMad Method Agile-AI Driven-Development"
|
||||||
default_selected: false # This module will be selected by default for new installations
|
default_selected: true # This module will be selected by default for new installations
|
||||||
|
|
||||||
header: "BMad Method™: Breakthrough Method of Agile-Ai Driven-Dev"
|
header: "BMad Method™: Breakthrough Method of Agile-Ai Driven-Dev"
|
||||||
subheader: "Agent and Workflow Configuration for this module"
|
subheader: "Agent and Workflow Configuration for this module"
|
||||||
|
|
@ -8,7 +8,7 @@ const chalk = require('chalk');
|
||||||
*
|
*
|
||||||
* @param {Object} options - Installation options
|
* @param {Object} options - Installation options
|
||||||
* @param {string} options.projectRoot - The root directory of the target project
|
* @param {string} options.projectRoot - The root directory of the target project
|
||||||
* @param {Object} options.config - Module configuration from install-config.yaml
|
* @param {Object} options.config - Module configuration from module.yaml
|
||||||
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
||||||
* @param {Object} options.logger - Logger instance for output
|
* @param {Object} options.logger - Logger instance for output
|
||||||
* @returns {Promise<boolean>} - Success status
|
* @returns {Promise<boolean>} - Success status
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ The installer is a multi-stage system that handles agent compilation, IDE integr
|
||||||
```
|
```
|
||||||
1. Collect User Input
|
1. Collect User Input
|
||||||
- Target directory, modules, IDEs
|
- Target directory, modules, IDEs
|
||||||
- Custom module configuration (via install-config.yaml)
|
- Custom module configuration (via module.yaml)
|
||||||
|
|
||||||
2. Pre-Installation
|
2. Pre-Installation
|
||||||
- Validate target, check conflicts, backup existing installations
|
- Validate target, check conflicts, backup existing installations
|
||||||
|
|
@ -183,12 +183,12 @@ The installer supports **15 IDE environments** through a base-derived architectu
|
||||||
|
|
||||||
### Custom Module Configuration
|
### Custom Module Configuration
|
||||||
|
|
||||||
Modules define interactive configuration menus via `install-config.yaml` files in their `_module-installer/` directories.
|
Modules define interactive configuration menus via `module.yaml` files in their `_module-installer/` directories.
|
||||||
|
|
||||||
**Config File Location**:
|
**Config File Location**:
|
||||||
|
|
||||||
- Core: `src/core/_module-installer/install-config.yaml`
|
- Core: `src/core/module.yaml`
|
||||||
- Modules: `src/modules/{module}/_module-installer/install-config.yaml`
|
- Modules: `src/modules/{module}/module.yaml`
|
||||||
|
|
||||||
**Configuration Types**:
|
**Configuration Types**:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,12 @@ class ConfigCollector {
|
||||||
* Collect configuration for all modules
|
* Collect configuration for all modules
|
||||||
* @param {Array} modules - List of modules to configure (including 'core')
|
* @param {Array} modules - List of modules to configure (including 'core')
|
||||||
* @param {string} projectDir - Target project directory
|
* @param {string} projectDir - Target project directory
|
||||||
|
* @param {Object} options - Additional options
|
||||||
|
* @param {Map} options.customModulePaths - Map of module ID to source path for custom modules
|
||||||
*/
|
*/
|
||||||
async collectAllConfigurations(modules, projectDir) {
|
async collectAllConfigurations(modules, projectDir, options = {}) {
|
||||||
|
// Store custom module paths for use in collectModuleConfig
|
||||||
|
this.customModulePaths = options.customModulePaths || new Map();
|
||||||
await this.loadExistingConfig(projectDir);
|
await this.loadExistingConfig(projectDir);
|
||||||
|
|
||||||
// Check if core was already collected (e.g., in early collection phase)
|
// Check if core was already collected (e.g., in early collection phase)
|
||||||
|
|
@ -183,24 +187,28 @@ class ConfigCollector {
|
||||||
|
|
||||||
// Load module's install config schema
|
// Load module's install config schema
|
||||||
// First, try the standard src/modules location
|
// First, try the standard src/modules location
|
||||||
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml');
|
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
|
||||||
|
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||||
|
|
||||||
// If not found in src/modules, we need to find it by searching the project
|
// If not found in src/modules, we need to find it by searching the project
|
||||||
if (!(await fs.pathExists(installerConfigPath))) {
|
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
|
||||||
// Use the module manager to find the module source
|
// Use the module manager to find the module source
|
||||||
const { ModuleManager } = require('../modules/manager');
|
const { ModuleManager } = require('../modules/manager');
|
||||||
const moduleManager = new ModuleManager();
|
const moduleManager = new ModuleManager();
|
||||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||||
|
|
||||||
if (moduleSourcePath) {
|
if (moduleSourcePath) {
|
||||||
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'install-config.yaml');
|
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
|
||||||
|
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let configPath = null;
|
let configPath = null;
|
||||||
let isCustomModule = false;
|
let isCustomModule = false;
|
||||||
|
|
||||||
if (await fs.pathExists(installerConfigPath)) {
|
if (await fs.pathExists(moduleConfigPath)) {
|
||||||
|
configPath = moduleConfigPath;
|
||||||
|
} else if (await fs.pathExists(installerConfigPath)) {
|
||||||
configPath = installerConfigPath;
|
configPath = installerConfigPath;
|
||||||
} else {
|
} else {
|
||||||
// Check if this is a custom module with custom.yaml
|
// Check if this is a custom module with custom.yaml
|
||||||
|
|
@ -447,23 +455,37 @@ class ConfigCollector {
|
||||||
this.allAnswers = {};
|
this.allAnswers = {};
|
||||||
}
|
}
|
||||||
// Load module's config
|
// Load module's config
|
||||||
// First, try the standard src/modules location
|
// First, check if we have a custom module path for this module
|
||||||
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml');
|
let installerConfigPath = null;
|
||||||
|
let moduleConfigPath = null;
|
||||||
|
|
||||||
// If not found in src/modules, we need to find it by searching the project
|
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
|
||||||
if (!(await fs.pathExists(installerConfigPath))) {
|
const customPath = this.customModulePaths.get(moduleName);
|
||||||
|
installerConfigPath = path.join(customPath, '_module-installer', 'module.yaml');
|
||||||
|
moduleConfigPath = path.join(customPath, 'module.yaml');
|
||||||
|
} else {
|
||||||
|
// Try the standard src/modules location
|
||||||
|
installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
|
||||||
|
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found in src/modules or custom paths, search the project
|
||||||
|
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
|
||||||
// Use the module manager to find the module source
|
// Use the module manager to find the module source
|
||||||
const { ModuleManager } = require('../modules/manager');
|
const { ModuleManager } = require('../modules/manager');
|
||||||
const moduleManager = new ModuleManager();
|
const moduleManager = new ModuleManager();
|
||||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||||
|
|
||||||
if (moduleSourcePath) {
|
if (moduleSourcePath) {
|
||||||
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'install-config.yaml');
|
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
|
||||||
|
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let configPath = null;
|
let configPath = null;
|
||||||
if (await fs.pathExists(installerConfigPath)) {
|
if (await fs.pathExists(moduleConfigPath)) {
|
||||||
|
configPath = moduleConfigPath;
|
||||||
|
} else if (await fs.pathExists(installerConfigPath)) {
|
||||||
configPath = installerConfigPath;
|
configPath = installerConfigPath;
|
||||||
} else {
|
} else {
|
||||||
// No config for this module
|
// No config for this module
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
/**
|
||||||
|
* Custom Module Source Cache
|
||||||
|
* Caches custom module sources under _cfg/custom/ to ensure they're never lost
|
||||||
|
* and can be checked into source control
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('node:path');
|
||||||
|
const crypto = require('node:crypto');
|
||||||
|
|
||||||
|
class CustomModuleCache {
|
||||||
|
constructor(bmadDir) {
|
||||||
|
this.bmadDir = bmadDir;
|
||||||
|
this.customCacheDir = path.join(bmadDir, '_cfg', 'custom');
|
||||||
|
this.manifestPath = path.join(this.customCacheDir, 'cache-manifest.yaml');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the custom cache directory exists
|
||||||
|
*/
|
||||||
|
async ensureCacheDir() {
|
||||||
|
await fs.ensureDir(this.customCacheDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache manifest
|
||||||
|
*/
|
||||||
|
async getCacheManifest() {
|
||||||
|
if (!(await fs.pathExists(this.manifestPath))) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await fs.readFile(this.manifestPath, 'utf8');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
return yaml.load(content) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update cache manifest
|
||||||
|
*/
|
||||||
|
async updateCacheManifest(manifest) {
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const content = yaml.dump(manifest, {
|
||||||
|
indent: 2,
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true,
|
||||||
|
sortKeys: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile(this.manifestPath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate hash of a file or directory
|
||||||
|
*/
|
||||||
|
async calculateHash(sourcePath) {
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
|
||||||
|
const isDir = (await fs.stat(sourcePath)).isDirectory();
|
||||||
|
|
||||||
|
if (isDir) {
|
||||||
|
// For directories, hash all files
|
||||||
|
const files = [];
|
||||||
|
async function collectFiles(dir) {
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isFile()) {
|
||||||
|
files.push(path.join(dir, entry.name));
|
||||||
|
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
||||||
|
await collectFiles(path.join(dir, entry.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await collectFiles(sourcePath);
|
||||||
|
files.sort(); // Ensure consistent order
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const content = await fs.readFile(file);
|
||||||
|
const relativePath = path.relative(sourcePath, file);
|
||||||
|
hash.update(relativePath + '|' + content.toString('base64'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For single files
|
||||||
|
const content = await fs.readFile(sourcePath);
|
||||||
|
hash.update(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache a custom module source
|
||||||
|
* @param {string} moduleId - Module ID
|
||||||
|
* @param {string} sourcePath - Original source path
|
||||||
|
* @param {Object} metadata - Additional metadata to store
|
||||||
|
* @returns {Object} Cached module info
|
||||||
|
*/
|
||||||
|
async cacheModule(moduleId, sourcePath, metadata = {}) {
|
||||||
|
await this.ensureCacheDir();
|
||||||
|
|
||||||
|
const cacheDir = path.join(this.customCacheDir, moduleId);
|
||||||
|
const cacheManifest = await this.getCacheManifest();
|
||||||
|
|
||||||
|
// Check if already cached and unchanged
|
||||||
|
if (cacheManifest[moduleId]) {
|
||||||
|
const cached = cacheManifest[moduleId];
|
||||||
|
if (cached.originalHash && cached.originalHash === (await this.calculateHash(sourcePath))) {
|
||||||
|
// Source unchanged, return existing cache info
|
||||||
|
return {
|
||||||
|
moduleId,
|
||||||
|
cachePath: cacheDir,
|
||||||
|
...cached,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove existing cache if it exists
|
||||||
|
if (await fs.pathExists(cacheDir)) {
|
||||||
|
await fs.remove(cacheDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy module to cache
|
||||||
|
await fs.copy(sourcePath, cacheDir, {
|
||||||
|
filter: (src) => {
|
||||||
|
const relative = path.relative(sourcePath, src);
|
||||||
|
// Skip node_modules, .git, and other common ignore patterns
|
||||||
|
return !relative.includes('node_modules') && !relative.startsWith('.git') && !relative.startsWith('.DS_Store');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate hash of the source
|
||||||
|
const sourceHash = await this.calculateHash(sourcePath);
|
||||||
|
const cacheHash = await this.calculateHash(cacheDir);
|
||||||
|
|
||||||
|
// Update manifest - don't store originalPath for source control friendliness
|
||||||
|
cacheManifest[moduleId] = {
|
||||||
|
originalHash: sourceHash,
|
||||||
|
cacheHash: cacheHash,
|
||||||
|
cachedAt: new Date().toISOString(),
|
||||||
|
...metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.updateCacheManifest(cacheManifest);
|
||||||
|
|
||||||
|
return {
|
||||||
|
moduleId,
|
||||||
|
cachePath: cacheDir,
|
||||||
|
...cacheManifest[moduleId],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached module info
|
||||||
|
* @param {string} moduleId - Module ID
|
||||||
|
* @returns {Object|null} Cached module info or null
|
||||||
|
*/
|
||||||
|
async getCachedModule(moduleId) {
|
||||||
|
const cacheManifest = await this.getCacheManifest();
|
||||||
|
const cached = cacheManifest[moduleId];
|
||||||
|
|
||||||
|
if (!cached) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheDir = path.join(this.customCacheDir, moduleId);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(cacheDir))) {
|
||||||
|
// Cache dir missing, remove from manifest
|
||||||
|
delete cacheManifest[moduleId];
|
||||||
|
await this.updateCacheManifest(cacheManifest);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify cache integrity
|
||||||
|
const currentCacheHash = await this.calculateHash(cacheDir);
|
||||||
|
if (currentCacheHash !== cached.cacheHash) {
|
||||||
|
console.warn(`Warning: Cache integrity check failed for ${moduleId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
moduleId,
|
||||||
|
cachePath: cacheDir,
|
||||||
|
...cached,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cached modules
|
||||||
|
* @returns {Array} Array of cached module info
|
||||||
|
*/
|
||||||
|
async getAllCachedModules() {
|
||||||
|
const cacheManifest = await this.getCacheManifest();
|
||||||
|
const cached = [];
|
||||||
|
|
||||||
|
for (const [moduleId, info] of Object.entries(cacheManifest)) {
|
||||||
|
const cachedModule = await this.getCachedModule(moduleId);
|
||||||
|
if (cachedModule) {
|
||||||
|
cached.push(cachedModule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a cached module
|
||||||
|
* @param {string} moduleId - Module ID to remove
|
||||||
|
*/
|
||||||
|
async removeCachedModule(moduleId) {
|
||||||
|
const cacheManifest = await this.getCacheManifest();
|
||||||
|
const cacheDir = path.join(this.customCacheDir, moduleId);
|
||||||
|
|
||||||
|
// Remove cache directory
|
||||||
|
if (await fs.pathExists(cacheDir)) {
|
||||||
|
await fs.remove(cacheDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from manifest
|
||||||
|
delete cacheManifest[moduleId];
|
||||||
|
await this.updateCacheManifest(cacheManifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync cached modules with a list of module IDs
|
||||||
|
* @param {Array<string>} moduleIds - Module IDs to keep
|
||||||
|
*/
|
||||||
|
async syncCache(moduleIds) {
|
||||||
|
const cached = await this.getAllCachedModules();
|
||||||
|
|
||||||
|
for (const cachedModule of cached) {
|
||||||
|
if (!moduleIds.includes(cachedModule.moduleId)) {
|
||||||
|
await this.removeCachedModule(cachedModule.moduleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { CustomModuleCache };
|
||||||
|
|
@ -17,6 +17,7 @@ class Detector {
|
||||||
hasCore: false,
|
hasCore: false,
|
||||||
modules: [],
|
modules: [],
|
||||||
ides: [],
|
ides: [],
|
||||||
|
customModules: [],
|
||||||
manifest: null,
|
manifest: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -32,6 +33,10 @@ class Detector {
|
||||||
result.manifest = manifestData;
|
result.manifest = manifestData;
|
||||||
result.version = manifestData.version;
|
result.version = manifestData.version;
|
||||||
result.installed = true;
|
result.installed = true;
|
||||||
|
// Copy custom modules if they exist
|
||||||
|
if (manifestData.customModules) {
|
||||||
|
result.customModules = manifestData.customModules;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for core
|
// Check for core
|
||||||
|
|
@ -275,10 +280,9 @@ class Detector {
|
||||||
hasV6Installation = true;
|
hasV6Installation = true;
|
||||||
// Don't break - continue scanning to be thorough
|
// Don't break - continue scanning to be thorough
|
||||||
} else {
|
} else {
|
||||||
// Not V6+, check if folder name contains "bmad" (case insensitive)
|
// Not V6+, check if this is the exact V4 folder name "bmad-method"
|
||||||
const nameLower = name.toLowerCase();
|
if (name === 'bmad-method') {
|
||||||
if (nameLower.includes('bmad')) {
|
// This is the V4 default folder - flag it as legacy
|
||||||
// Potential V4 legacy folder
|
|
||||||
potentialV4Folders.push(fullPath);
|
potentialV4Folders.push(fullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const ora = require('ora');
|
const ora = require('ora');
|
||||||
|
const inquirer = require('inquirer');
|
||||||
const { Detector } = require('./detector');
|
const { Detector } = require('./detector');
|
||||||
const { Manifest } = require('./manifest');
|
const { Manifest } = require('./manifest');
|
||||||
const { ModuleManager } = require('../modules/manager');
|
const { ModuleManager } = require('../modules/manager');
|
||||||
|
|
@ -129,7 +130,7 @@ class Installer {
|
||||||
*/
|
*/
|
||||||
async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
|
async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
|
||||||
// List of text file extensions that should have placeholder replacement
|
// List of text file extensions that should have placeholder replacement
|
||||||
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv'];
|
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
|
||||||
const ext = path.extname(sourcePath).toLowerCase();
|
const ext = path.extname(sourcePath).toLowerCase();
|
||||||
|
|
||||||
// Check if this is a text file that might contain placeholders
|
// Check if this is a text file that might contain placeholders
|
||||||
|
|
@ -434,8 +435,53 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
// Quick update already collected all configs, use them directly
|
// Quick update already collected all configs, use them directly
|
||||||
moduleConfigs = this.configCollector.collectedConfig;
|
moduleConfigs = this.configCollector.collectedConfig;
|
||||||
} else {
|
} else {
|
||||||
|
// Build custom module paths map from customContent
|
||||||
|
const customModulePaths = new Map();
|
||||||
|
|
||||||
|
// Handle selectedFiles (from existing install path or manual directory input)
|
||||||
|
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
|
||||||
|
const { CustomHandler } = require('../custom/handler');
|
||||||
|
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 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
|
||||||
|
const allModulesForConfig = [...(config.modules || [])];
|
||||||
|
for (const [moduleId] of customModulePaths) {
|
||||||
|
if (!allModulesForConfig.includes(moduleId)) {
|
||||||
|
allModulesForConfig.push(moduleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Regular install - collect configurations (core was already collected in UI.promptInstall if interactive)
|
// Regular install - collect configurations (core was already collected in UI.promptInstall if interactive)
|
||||||
moduleConfigs = await this.configCollector.collectAllConfigurations(config.modules || [], path.resolve(config.directory));
|
moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), {
|
||||||
|
customModulePaths,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get bmad_folder from config (default to 'bmad' for backwards compatibility)
|
// Get bmad_folder from config (default to 'bmad' for backwards compatibility)
|
||||||
|
|
@ -750,13 +796,81 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
spinner.text = 'Creating directory structure...';
|
spinner.text = 'Creating directory structure...';
|
||||||
await this.createDirectoryStructure(bmadDir);
|
await this.createDirectoryStructure(bmadDir);
|
||||||
|
|
||||||
// Resolve dependencies for selected modules
|
// Get project root
|
||||||
spinner.text = 'Resolving dependencies...';
|
|
||||||
const projectRoot = getProjectRoot();
|
const projectRoot = getProjectRoot();
|
||||||
const modulesToInstall = config.installCore ? ['core', ...config.modules] : config.modules;
|
|
||||||
|
// 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
|
||||||
|
for (const customFile of finalCustomContent.selectedFiles) {
|
||||||
|
const { CustomHandler } = require('../custom/handler');
|
||||||
|
const customHandler = new CustomHandler();
|
||||||
|
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 need to pass the project root
|
// For dependency resolution, we need to pass the project root
|
||||||
const resolution = await this.dependencyResolver.resolve(projectRoot, config.modules || [], { verbose: config.verbose });
|
// Create a temporary module manager that knows about custom content locations
|
||||||
|
const tempModuleManager = new ModuleManager({
|
||||||
|
scanProjectForModules: true,
|
||||||
|
bmadDir: bmadDir, // Pass bmadDir so we can check cache
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make sure custom modules are discoverable
|
||||||
|
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
|
||||||
|
// The dependency resolver needs to know about these modules
|
||||||
|
// We'll handle custom modules separately in the installation loop
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolution = await this.dependencyResolver.resolve(projectRoot, allModules, {
|
||||||
|
verbose: config.verbose,
|
||||||
|
moduleManager: tempModuleManager,
|
||||||
|
});
|
||||||
|
|
||||||
if (config.verbose) {
|
if (config.verbose) {
|
||||||
spinner.succeed('Dependencies resolved');
|
spinner.succeed('Dependencies resolved');
|
||||||
|
|
@ -764,24 +878,164 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
spinner.succeed('Dependencies resolved');
|
spinner.succeed('Dependencies resolved');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install core if requested or if dependencies require it
|
// Core is already installed above, skip if included in resolution
|
||||||
if (config.installCore || resolution.byModule.core) {
|
|
||||||
spinner.start('Installing BMAD core...');
|
|
||||||
await this.installCoreWithDependencies(bmadDir, resolution.byModule.core);
|
|
||||||
spinner.succeed('Core installed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install modules with their dependencies
|
// Install modules with their dependencies
|
||||||
if (config.modules && config.modules.length > 0) {
|
if (allModules && allModules.length > 0) {
|
||||||
for (const moduleName of config.modules) {
|
const installedModuleNames = new Set();
|
||||||
|
|
||||||
|
for (const moduleName of allModules) {
|
||||||
|
// Skip if already installed
|
||||||
|
if (installedModuleNames.has(moduleName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
installedModuleNames.add(moduleName);
|
||||||
|
|
||||||
spinner.start(`Installing module: ${moduleName}...`);
|
spinner.start(`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 _cfg)
|
||||||
|
if (customInfo.sourcePath && (customInfo.sourcePath.startsWith('_cfg') || customInfo.sourcePath.includes('_cfg/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 } = require('../custom/handler');
|
||||||
|
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) {
|
||||||
|
// Install custom module using CustomHandler but as a proper module
|
||||||
|
const { CustomHandler } = require('../custom/handler');
|
||||||
|
const customHandler = new CustomHandler();
|
||||||
|
|
||||||
|
// Install to module directory instead of custom directory
|
||||||
|
const moduleTargetPath = path.join(bmadDir, moduleName);
|
||||||
|
await fs.ensureDir(moduleTargetPath);
|
||||||
|
|
||||||
|
// Get collected config for this custom module (from module.yaml prompts)
|
||||||
|
const collectedModuleConfig = moduleConfigs[moduleName] || {};
|
||||||
|
|
||||||
|
const result = await customHandler.install(
|
||||||
|
customInfo.path,
|
||||||
|
path.join(bmadDir, 'temp-custom'),
|
||||||
|
{ ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig, _bmadDir: bmadDir },
|
||||||
|
(filePath) => {
|
||||||
|
// Track installed files with correct path
|
||||||
|
const relativePath = path.relative(path.join(bmadDir, 'temp-custom'), filePath);
|
||||||
|
const finalPath = path.join(moduleTargetPath, relativePath);
|
||||||
|
this.installedFiles.push(finalPath);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Move from temp-custom to actual module directory
|
||||||
|
const tempCustomPath = path.join(bmadDir, 'temp-custom');
|
||||||
|
if (await fs.pathExists(tempCustomPath)) {
|
||||||
|
const customDir = path.join(tempCustomPath, 'custom');
|
||||||
|
if (await fs.pathExists(customDir)) {
|
||||||
|
// Move contents to module directory
|
||||||
|
const items = await fs.readdir(customDir);
|
||||||
|
for (const item of items) {
|
||||||
|
const srcPath = path.join(customDir, item);
|
||||||
|
const destPath = path.join(moduleTargetPath, item);
|
||||||
|
|
||||||
|
// If destination exists, remove it first (or we could merge)
|
||||||
|
if (await fs.pathExists(destPath)) {
|
||||||
|
await fs.remove(destPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.move(srcPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fs.remove(tempCustomPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create module config (include collected config from module.yaml prompts)
|
||||||
|
await this.generateModuleConfigs(bmadDir, {
|
||||||
|
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store custom module info for later manifest update
|
||||||
|
if (!config._customModulesToTrack) {
|
||||||
|
config._customModulesToTrack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// For cached modules, use appropriate path handling
|
||||||
|
let sourcePath;
|
||||||
|
if (useCache) {
|
||||||
|
// Check if we have cached modules info (from initial install)
|
||||||
|
if (finalCustomContent && finalCustomContent.cachedModules) {
|
||||||
|
sourcePath = finalCustomContent.cachedModules.find((m) => m.id === moduleName)?.relativePath;
|
||||||
|
} else {
|
||||||
|
// During update, the sourcePath is already cache-relative if it starts with _cfg
|
||||||
|
sourcePath =
|
||||||
|
customInfo.sourcePath && customInfo.sourcePath.startsWith('_cfg')
|
||||||
|
? customInfo.sourcePath
|
||||||
|
: path.relative(bmadDir, customInfo.path || customInfo.sourcePath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sourcePath = path.resolve(customInfo.path || customInfo.sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
config._customModulesToTrack.push({
|
||||||
|
id: customInfo.id,
|
||||||
|
name: customInfo.name,
|
||||||
|
sourcePath: sourcePath,
|
||||||
|
installDate: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
} 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]);
|
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
spinner.succeed(`Module installed: ${moduleName}`);
|
spinner.succeed(`Module installed: ${moduleName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install partial modules (only dependencies)
|
// Install partial modules (only dependencies)
|
||||||
for (const [module, files] of Object.entries(resolution.byModule)) {
|
for (const [module, files] of Object.entries(resolution.byModule)) {
|
||||||
if (!config.modules.includes(module) && module !== 'core') {
|
if (!allModules.includes(module) && module !== 'core') {
|
||||||
const totalFiles =
|
const totalFiles =
|
||||||
files.agents.length +
|
files.agents.length +
|
||||||
files.tasks.length +
|
files.tasks.length +
|
||||||
|
|
@ -799,6 +1053,11 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install custom content if provided AND selected
|
// Install custom content if provided AND selected
|
||||||
|
// Process custom content that wasn't installed as modules
|
||||||
|
// This is now handled in the module installation loop above
|
||||||
|
// This section is kept for backward compatibility with any custom content
|
||||||
|
// that doesn't have a module structure
|
||||||
|
const remainingCustomContent = [];
|
||||||
if (
|
if (
|
||||||
config.customContent &&
|
config.customContent &&
|
||||||
config.customContent.hasCustomContent &&
|
config.customContent.hasCustomContent &&
|
||||||
|
|
@ -806,12 +1065,26 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
config.customContent.selected &&
|
config.customContent.selected &&
|
||||||
config.customContent.selectedFiles
|
config.customContent.selectedFiles
|
||||||
) {
|
) {
|
||||||
spinner.start('Installing custom content...');
|
// Filter out custom modules that were already installed
|
||||||
|
for (const customFile of config.customContent.selectedFiles) {
|
||||||
|
const { CustomHandler } = require('../custom/handler');
|
||||||
|
const customHandler = new CustomHandler();
|
||||||
|
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
|
||||||
|
|
||||||
|
// Skip if this was installed as a module
|
||||||
|
if (!customInfo || !customInfo.id || !allModules.includes(customInfo.id)) {
|
||||||
|
remainingCustomContent.push(customFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingCustomContent.length > 0) {
|
||||||
|
spinner.start('Installing remaining custom content...');
|
||||||
const { CustomHandler } = require('../custom/handler');
|
const { CustomHandler } = require('../custom/handler');
|
||||||
const customHandler = new CustomHandler();
|
const customHandler = new CustomHandler();
|
||||||
|
|
||||||
// Use the selected files instead of finding all files
|
// Use the remaining files
|
||||||
const customFiles = config.customContent.selectedFiles;
|
const customFiles = remainingCustomContent;
|
||||||
|
|
||||||
if (customFiles.length > 0) {
|
if (customFiles.length > 0) {
|
||||||
console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`));
|
console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`));
|
||||||
|
|
@ -867,14 +1140,37 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
spinner.start('Generating workflow and agent manifests...');
|
spinner.start('Generating workflow and agent manifests...');
|
||||||
const manifestGen = new ManifestGenerator();
|
const manifestGen = new ManifestGenerator();
|
||||||
|
|
||||||
// Include preserved modules (from quick update) in the manifest
|
// For quick update, we need ALL installed modules in the manifest
|
||||||
const allModulesToList = config._preserveModules ? [...(config.modules || []), ...config._preserveModules] : config.modules || [];
|
// Not just the ones being updated
|
||||||
|
const allModulesForManifest = config._quickUpdate
|
||||||
|
? config._existingModules || allModules || []
|
||||||
|
: config._preserveModules
|
||||||
|
? [...allModules, ...config._preserveModules]
|
||||||
|
: allModules || [];
|
||||||
|
|
||||||
const manifestStats = await manifestGen.generateManifests(bmadDir, config.modules || [], this.installedFiles, {
|
// 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 || [],
|
ides: config.ides || [],
|
||||||
preservedModules: config._preserveModules || [], // Scan these from installed bmad/ dir
|
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add custom modules to manifest (now that it exists)
|
||||||
|
if (config._customModulesToTrack && config._customModulesToTrack.length > 0) {
|
||||||
|
spinner.text = 'Storing custom module sources...';
|
||||||
|
for (const customModule of config._customModulesToTrack) {
|
||||||
|
await this.manifest.addCustomModule(bmadDir, customModule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
spinner.succeed(
|
spinner.succeed(
|
||||||
`Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
|
`Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
|
||||||
);
|
);
|
||||||
|
|
@ -1137,6 +1433,30 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
const currentVersion = existingInstall.version;
|
const currentVersion = existingInstall.version;
|
||||||
const newVersion = require(path.join(getProjectRoot(), 'package.json')).version;
|
const newVersion = require(path.join(getProjectRoot(), 'package.json')).version;
|
||||||
|
|
||||||
|
// Check for custom modules with missing sources before update
|
||||||
|
const customModuleSources = new Map();
|
||||||
|
if (existingInstall.customModules) {
|
||||||
|
for (const customModule of existingInstall.customModules) {
|
||||||
|
customModuleSources.set(customModule.id, customModule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
if (config.dryRun) {
|
||||||
spinner.stop();
|
spinner.stop();
|
||||||
console.log(chalk.cyan('\n🔍 Update Preview (Dry Run)\n'));
|
console.log(chalk.cyan('\n🔍 Update Preview (Dry Run)\n'));
|
||||||
|
|
@ -1594,6 +1914,9 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
|
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
|
||||||
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
|
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
|
||||||
|
|
||||||
|
// Replace {bmad_folder} with actual folder name
|
||||||
|
xmlContent = xmlContent.replaceAll('{bmad_folder}', this.bmadFolderName || 'bmad');
|
||||||
|
|
||||||
// Replace {agent_sidecar_folder} if configured
|
// Replace {agent_sidecar_folder} if configured
|
||||||
const coreConfig = this.configCollector.collectedConfig.core || {};
|
const coreConfig = this.configCollector.collectedConfig.core || {};
|
||||||
if (coreConfig.agent_sidecar_folder && xmlContent.includes('{agent_sidecar_folder}')) {
|
if (coreConfig.agent_sidecar_folder && xmlContent.includes('{agent_sidecar_folder}')) {
|
||||||
|
|
@ -1905,6 +2228,24 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
throw new Error(`BMAD not installed at ${bmadDir}`);
|
throw new Error(`BMAD not installed at ${bmadDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for custom modules with missing sources
|
||||||
|
const manifest = await this.manifest.read(bmadDir);
|
||||||
|
if (manifest && manifest.customModules && manifest.customModules.length > 0) {
|
||||||
|
spinner.stop();
|
||||||
|
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();
|
||||||
|
const installedModules = manifest.modules || [];
|
||||||
|
await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules);
|
||||||
|
|
||||||
|
spinner.start('Rebuilding agent files...');
|
||||||
|
}
|
||||||
|
|
||||||
let agentCount = 0;
|
let agentCount = 0;
|
||||||
let taskCount = 0;
|
let taskCount = 0;
|
||||||
|
|
||||||
|
|
@ -2049,17 +2390,245 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
const existingInstall = await this.detector.detect(bmadDir);
|
const existingInstall = await this.detector.detect(bmadDir);
|
||||||
const installedModules = existingInstall.modules.map((m) => m.id);
|
const installedModules = existingInstall.modules.map((m) => m.id);
|
||||||
const configuredIdes = existingInstall.ides || [];
|
const configuredIdes = existingInstall.ides || [];
|
||||||
|
const projectRoot = path.dirname(bmadDir);
|
||||||
|
|
||||||
|
// Get custom module sources from manifest
|
||||||
|
const customModuleSources = new Map();
|
||||||
|
if (existingInstall.customModules) {
|
||||||
|
for (const customModule of existingInstall.customModules) {
|
||||||
|
// Ensure we have an absolute sourcePath
|
||||||
|
let absoluteSourcePath = customModule.sourcePath;
|
||||||
|
|
||||||
|
// Check if sourcePath is a cache-relative path (starts with _cfg/)
|
||||||
|
if (absoluteSourcePath && absoluteSourcePath.startsWith('_cfg')) {
|
||||||
|
// 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(projectRoot, customModule.relativePath);
|
||||||
|
}
|
||||||
|
// Ensure sourcePath is absolute for anything else
|
||||||
|
else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
|
||||||
|
absoluteSourcePath = path.resolve(absoluteSourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the custom module object with the absolute path
|
||||||
|
const updatedModule = {
|
||||||
|
...customModule,
|
||||||
|
sourcePath: absoluteSourcePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
customModuleSources.set(customModule.id, updatedModule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load saved IDE configurations
|
// Load saved IDE configurations
|
||||||
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
||||||
|
|
||||||
// Get available modules (what we have source for)
|
// Get available modules (what we have source for)
|
||||||
const availableModules = await this.moduleManager.listAvailable();
|
const availableModulesData = await this.moduleManager.listAvailable();
|
||||||
const availableModuleIds = new Set(availableModules.map((m) => m.id));
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for untracked custom modules (installed but not in manifest)
|
||||||
|
const untrackedCustomModules = [];
|
||||||
|
for (const installedModule of installedModules) {
|
||||||
|
// Skip standard modules and core
|
||||||
|
const standardModuleIds = ['bmb', 'bmgd', 'bmm', 'cis', 'core'];
|
||||||
|
if (standardModuleIds.includes(installedModule)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this installed module is not tracked in customModules
|
||||||
|
if (!customModuleSources.has(installedModule)) {
|
||||||
|
const modulePath = path.join(bmadDir, installedModule);
|
||||||
|
if (await fs.pathExists(modulePath)) {
|
||||||
|
untrackedCustomModules.push({
|
||||||
|
id: installedModule,
|
||||||
|
name: installedModule, // We don't have the original name
|
||||||
|
path: modulePath,
|
||||||
|
untracked: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found untracked custom modules, offer to track them
|
||||||
|
if (untrackedCustomModules.length > 0) {
|
||||||
|
spinner.stop();
|
||||||
|
console.log(chalk.yellow(`\n⚠️ Found ${untrackedCustomModules.length} custom module(s) not tracked in manifest:`));
|
||||||
|
|
||||||
|
for (const untracked of untrackedCustomModules) {
|
||||||
|
console.log(chalk.dim(` • ${untracked.id} (installed at ${path.relative(projectRoot, untracked.path)})`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { trackModules } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'trackModules',
|
||||||
|
message: chalk.cyan('Would you like to scan for their source locations?'),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (trackModules) {
|
||||||
|
const { scanDirectory } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'scanDirectory',
|
||||||
|
message: 'Enter directory to scan for custom module sources (or leave blank to skip):',
|
||||||
|
default: projectRoot,
|
||||||
|
validate: async (input) => {
|
||||||
|
if (input && input.trim() !== '') {
|
||||||
|
const expandedPath = path.resolve(input.trim());
|
||||||
|
if (!(await fs.pathExists(expandedPath))) {
|
||||||
|
return 'Directory does not exist';
|
||||||
|
}
|
||||||
|
const stats = await fs.stat(expandedPath);
|
||||||
|
if (!stats.isDirectory()) {
|
||||||
|
return 'Path must be a directory';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (scanDirectory && scanDirectory.trim() !== '') {
|
||||||
|
console.log(chalk.dim('\nScanning for custom module sources...'));
|
||||||
|
|
||||||
|
// Scan for all module.yaml files
|
||||||
|
const allModulePaths = await this.moduleManager.findModulesInProject(scanDirectory);
|
||||||
|
const { ModuleManager } = require('../modules/manager');
|
||||||
|
const mm = new ModuleManager({ scanProjectForModules: true });
|
||||||
|
|
||||||
|
for (const untracked of untrackedCustomModules) {
|
||||||
|
let foundSource = null;
|
||||||
|
|
||||||
|
// Try to find by module ID
|
||||||
|
for (const modulePath of allModulePaths) {
|
||||||
|
try {
|
||||||
|
const moduleInfo = await mm.getModuleInfo(modulePath);
|
||||||
|
if (moduleInfo && moduleInfo.id === untracked.id) {
|
||||||
|
foundSource = {
|
||||||
|
path: modulePath,
|
||||||
|
info: moduleInfo,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Continue searching
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundSource) {
|
||||||
|
console.log(chalk.green(` ✓ Found source for ${untracked.id}: ${path.relative(projectRoot, foundSource.path)}`));
|
||||||
|
|
||||||
|
// Add to manifest
|
||||||
|
await this.manifest.addCustomModule(bmadDir, {
|
||||||
|
id: untracked.id,
|
||||||
|
name: foundSource.info.name || untracked.name,
|
||||||
|
sourcePath: path.resolve(foundSource.path),
|
||||||
|
installDate: new Date().toISOString(),
|
||||||
|
tracked: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to customModuleSources for processing
|
||||||
|
customModuleSources.set(untracked.id, {
|
||||||
|
id: untracked.id,
|
||||||
|
name: foundSource.info.name || untracked.name,
|
||||||
|
sourcePath: path.resolve(foundSource.path),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(chalk.yellow(` ⚠ Could not find source for ${untracked.id}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.dim('\nUntracked custom modules will remain installed but cannot be updated without their source.'));
|
||||||
|
spinner.start('Preparing update...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle missing custom module sources using shared method
|
||||||
|
const customModuleResult = await this.handleMissingCustomSources(
|
||||||
|
customModuleSources,
|
||||||
|
bmadDir,
|
||||||
|
projectRoot,
|
||||||
|
'update',
|
||||||
|
installedModules,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle both old return format (array) and new format (object)
|
||||||
|
let validCustomModules = [];
|
||||||
|
let keptModulesWithoutSources = [];
|
||||||
|
|
||||||
|
if (Array.isArray(customModuleResult)) {
|
||||||
|
// Old format - just an array
|
||||||
|
validCustomModules = customModuleResult;
|
||||||
|
} else if (customModuleResult && typeof customModuleResult === 'object') {
|
||||||
|
// New format - object with two arrays
|
||||||
|
validCustomModules = customModuleResult.validCustomModules || [];
|
||||||
|
keptModulesWithoutSources = customModuleResult.keptModulesWithoutSources || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const customModulesFromManifest = validCustomModules.map((m) => ({
|
||||||
|
...m,
|
||||||
|
isCustom: true,
|
||||||
|
hasUpdate: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add untracked modules to the update list but mark them as untrackable
|
||||||
|
for (const untracked of untrackedCustomModules) {
|
||||||
|
if (!customModuleSources.has(untracked.id)) {
|
||||||
|
customModulesFromManifest.push({
|
||||||
|
...untracked,
|
||||||
|
isCustom: true,
|
||||||
|
hasUpdate: false, // Can't update without source
|
||||||
|
untracked: 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)
|
// Only update modules that are BOTH installed AND available (we have source for)
|
||||||
const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
|
const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id));
|
||||||
const skippedModules = installedModules.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)`);
|
spinner.succeed(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`);
|
||||||
|
|
||||||
|
|
@ -2124,6 +2693,8 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
_quickUpdate: true, // Flag to skip certain prompts
|
_quickUpdate: true, // Flag to skip certain prompts
|
||||||
_preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
|
_preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
|
||||||
_savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer
|
_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
|
// Call the standard install method
|
||||||
|
|
@ -2763,6 +3334,230 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 };
|
module.exports = { Installer };
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,11 @@ class ManifestGenerator {
|
||||||
// Deduplicate modules list to prevent duplicates
|
// Deduplicate modules list to prevent duplicates
|
||||||
this.modules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])];
|
this.modules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])];
|
||||||
this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])]; // All installed modules get rescanned
|
this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])]; // All installed modules get rescanned
|
||||||
this.preservedModules = preservedModules; // These stay as-is in CSVs
|
|
||||||
|
// For CSV manifests, we need to include ALL modules that are installed
|
||||||
|
// preservedModules controls which modules stay as-is in the CSV (don't get rescanned)
|
||||||
|
// But all modules should be included in the final manifest
|
||||||
|
this.preservedModules = [...new Set([...preservedModules, ...selectedModules, ...installedModules])]; // Include all installed modules
|
||||||
this.bmadDir = bmadDir;
|
this.bmadDir = bmadDir;
|
||||||
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '.bmad' or 'bmad')
|
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '.bmad' or 'bmad')
|
||||||
this.allInstalledFiles = installedFiles;
|
this.allInstalledFiles = installedFiles;
|
||||||
|
|
@ -61,14 +65,14 @@ class ManifestGenerator {
|
||||||
// Collect workflow data
|
// Collect workflow data
|
||||||
await this.collectWorkflows(selectedModules);
|
await this.collectWorkflows(selectedModules);
|
||||||
|
|
||||||
// Collect agent data
|
// Collect agent data - use updatedModules which includes all installed modules
|
||||||
await this.collectAgents(selectedModules);
|
await this.collectAgents(this.updatedModules);
|
||||||
|
|
||||||
// Collect task data
|
// Collect task data
|
||||||
await this.collectTasks(selectedModules);
|
await this.collectTasks(this.updatedModules);
|
||||||
|
|
||||||
// Collect tool data
|
// Collect tool data
|
||||||
await this.collectTools(selectedModules);
|
await this.collectTools(this.updatedModules);
|
||||||
|
|
||||||
// Write manifest files and collect their paths
|
// Write manifest files and collect their paths
|
||||||
const manifestFiles = [
|
const manifestFiles = [
|
||||||
|
|
@ -450,6 +454,21 @@ class ManifestGenerator {
|
||||||
async writeMainManifest(cfgDir) {
|
async writeMainManifest(cfgDir) {
|
||||||
const manifestPath = path.join(cfgDir, 'manifest.yaml');
|
const manifestPath = path.join(cfgDir, 'manifest.yaml');
|
||||||
|
|
||||||
|
// Read existing manifest to preserve custom modules
|
||||||
|
let existingCustomModules = [];
|
||||||
|
if (await fs.pathExists(manifestPath)) {
|
||||||
|
try {
|
||||||
|
const existingContent = await fs.readFile(manifestPath, 'utf8');
|
||||||
|
const existingManifest = yaml.load(existingContent);
|
||||||
|
if (existingManifest && existingManifest.customModules) {
|
||||||
|
existingCustomModules = existingManifest.customModules;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If we can't read the existing manifest, continue without preserving custom modules
|
||||||
|
console.warn('Warning: Could not read existing manifest to preserve custom modules');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
installation: {
|
installation: {
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
|
|
@ -457,6 +476,7 @@ class ManifestGenerator {
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
modules: this.modules,
|
modules: this.modules,
|
||||||
|
customModules: existingCustomModules, // Preserve custom modules
|
||||||
ides: this.selectedIdes,
|
ides: this.selectedIdes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -562,12 +582,47 @@ class ManifestGenerator {
|
||||||
async writeWorkflowManifest(cfgDir) {
|
async writeWorkflowManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
|
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
|
||||||
|
|
||||||
|
// Read existing manifest to preserve entries
|
||||||
|
const existingEntries = new Map();
|
||||||
|
if (await fs.pathExists(csvPath)) {
|
||||||
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
|
const lines = content.split('\n').filter((line) => line.trim());
|
||||||
|
|
||||||
|
// Skip header
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line) {
|
||||||
|
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
||||||
|
const parts = line.split('","');
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
const name = parts[0].replace(/^"/, '');
|
||||||
|
const module = parts[2];
|
||||||
|
existingEntries.set(`${module}:${name}`, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create CSV header - removed standalone column as ALL workflows now generate commands
|
// Create CSV header - removed standalone column as ALL workflows now generate commands
|
||||||
let csv = 'name,description,module,path\n';
|
let csv = 'name,description,module,path\n';
|
||||||
|
|
||||||
// Add all workflows - no standalone property needed anymore
|
// Combine existing and new workflows
|
||||||
|
const allWorkflows = new Map();
|
||||||
|
|
||||||
|
// Add existing entries
|
||||||
|
for (const [key, value] of existingEntries) {
|
||||||
|
allWorkflows.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/update new workflows
|
||||||
for (const workflow of this.workflows) {
|
for (const workflow of this.workflows) {
|
||||||
csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"\n`;
|
const key = `${workflow.module}:${workflow.name}`;
|
||||||
|
allWorkflows.set(key, `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write all workflows
|
||||||
|
for (const [, value] of allWorkflows) {
|
||||||
|
csv += value + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csv);
|
await fs.writeFile(csvPath, csv);
|
||||||
|
|
@ -581,12 +636,50 @@ class ManifestGenerator {
|
||||||
async writeAgentManifest(cfgDir) {
|
async writeAgentManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
|
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
|
||||||
|
|
||||||
|
// Read existing manifest to preserve entries
|
||||||
|
const existingEntries = new Map();
|
||||||
|
if (await fs.pathExists(csvPath)) {
|
||||||
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
|
const lines = content.split('\n').filter((line) => line.trim());
|
||||||
|
|
||||||
|
// Skip header
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line) {
|
||||||
|
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
||||||
|
const parts = line.split('","');
|
||||||
|
if (parts.length >= 11) {
|
||||||
|
const name = parts[0].replace(/^"/, '');
|
||||||
|
const module = parts[8];
|
||||||
|
existingEntries.set(`${module}:${name}`, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create CSV header with persona fields
|
// Create CSV header with persona fields
|
||||||
let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
|
let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
|
||||||
|
|
||||||
// Add all agents
|
// Combine existing and new agents, preferring new data for duplicates
|
||||||
|
const allAgents = new Map();
|
||||||
|
|
||||||
|
// Add existing entries
|
||||||
|
for (const [key, value] of existingEntries) {
|
||||||
|
allAgents.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/update new agents
|
||||||
for (const agent of this.agents) {
|
for (const agent of this.agents) {
|
||||||
csv += `"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"\n`;
|
const key = `${agent.module}:${agent.name}`;
|
||||||
|
allAgents.set(
|
||||||
|
key,
|
||||||
|
`"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write all agents
|
||||||
|
for (const [, value] of allAgents) {
|
||||||
|
csv += value + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csv);
|
await fs.writeFile(csvPath, csv);
|
||||||
|
|
@ -600,12 +693,47 @@ class ManifestGenerator {
|
||||||
async writeTaskManifest(cfgDir) {
|
async writeTaskManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
||||||
|
|
||||||
|
// Read existing manifest to preserve entries
|
||||||
|
const existingEntries = new Map();
|
||||||
|
if (await fs.pathExists(csvPath)) {
|
||||||
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
|
const lines = content.split('\n').filter((line) => line.trim());
|
||||||
|
|
||||||
|
// Skip header
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line) {
|
||||||
|
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
||||||
|
const parts = line.split('","');
|
||||||
|
if (parts.length >= 6) {
|
||||||
|
const name = parts[0].replace(/^"/, '');
|
||||||
|
const module = parts[3];
|
||||||
|
existingEntries.set(`${module}:${name}`, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create CSV header with standalone column
|
// Create CSV header with standalone column
|
||||||
let csv = 'name,displayName,description,module,path,standalone\n';
|
let csv = 'name,displayName,description,module,path,standalone\n';
|
||||||
|
|
||||||
// Add all tasks
|
// Combine existing and new tasks
|
||||||
|
const allTasks = new Map();
|
||||||
|
|
||||||
|
// Add existing entries
|
||||||
|
for (const [key, value] of existingEntries) {
|
||||||
|
allTasks.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/update new tasks
|
||||||
for (const task of this.tasks) {
|
for (const task of this.tasks) {
|
||||||
csv += `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"\n`;
|
const key = `${task.module}:${task.name}`;
|
||||||
|
allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write all tasks
|
||||||
|
for (const [, value] of allTasks) {
|
||||||
|
csv += value + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csv);
|
await fs.writeFile(csvPath, csv);
|
||||||
|
|
@ -619,12 +747,47 @@ class ManifestGenerator {
|
||||||
async writeToolManifest(cfgDir) {
|
async writeToolManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
|
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
|
||||||
|
|
||||||
|
// Read existing manifest to preserve entries
|
||||||
|
const existingEntries = new Map();
|
||||||
|
if (await fs.pathExists(csvPath)) {
|
||||||
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
|
const lines = content.split('\n').filter((line) => line.trim());
|
||||||
|
|
||||||
|
// Skip header
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line) {
|
||||||
|
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
||||||
|
const parts = line.split('","');
|
||||||
|
if (parts.length >= 6) {
|
||||||
|
const name = parts[0].replace(/^"/, '');
|
||||||
|
const module = parts[3];
|
||||||
|
existingEntries.set(`${module}:${name}`, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create CSV header with standalone column
|
// Create CSV header with standalone column
|
||||||
let csv = 'name,displayName,description,module,path,standalone\n';
|
let csv = 'name,displayName,description,module,path,standalone\n';
|
||||||
|
|
||||||
// Add all tools
|
// Combine existing and new tools
|
||||||
|
const allTools = new Map();
|
||||||
|
|
||||||
|
// Add existing entries
|
||||||
|
for (const [key, value] of existingEntries) {
|
||||||
|
allTools.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/update new tools
|
||||||
for (const tool of this.tools) {
|
for (const tool of this.tools) {
|
||||||
csv += `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"\n`;
|
const key = `${tool.module}:${tool.name}`;
|
||||||
|
allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write all tools
|
||||||
|
for (const [, value] of allTools) {
|
||||||
|
csv += value + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csv);
|
await fs.writeFile(csvPath, csv);
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ class Manifest {
|
||||||
installDate: manifestData.installation?.installDate,
|
installDate: manifestData.installation?.installDate,
|
||||||
lastUpdated: manifestData.installation?.lastUpdated,
|
lastUpdated: manifestData.installation?.lastUpdated,
|
||||||
modules: manifestData.modules || [],
|
modules: manifestData.modules || [],
|
||||||
|
customModules: manifestData.customModules || [],
|
||||||
ides: manifestData.ides || [],
|
ides: manifestData.ides || [],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -93,6 +94,7 @@ class Manifest {
|
||||||
lastUpdated: manifest.lastUpdated,
|
lastUpdated: manifest.lastUpdated,
|
||||||
},
|
},
|
||||||
modules: manifest.modules || [],
|
modules: manifest.modules || [],
|
||||||
|
customModules: manifest.customModules || [],
|
||||||
ides: manifest.ides || [],
|
ides: manifest.ides || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -535,6 +537,51 @@ class Manifest {
|
||||||
|
|
||||||
return configs;
|
return configs;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Add a custom module to the manifest with its source path
|
||||||
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
* @param {Object} customModule - Custom module info
|
||||||
|
*/
|
||||||
|
async addCustomModule(bmadDir, customModule) {
|
||||||
|
const manifest = await this.read(bmadDir);
|
||||||
|
if (!manifest) {
|
||||||
|
throw new Error('No manifest found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manifest.customModules) {
|
||||||
|
manifest.customModules = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if custom module already exists
|
||||||
|
const existingIndex = manifest.customModules.findIndex((m) => m.id === customModule.id);
|
||||||
|
if (existingIndex === -1) {
|
||||||
|
// Add new entry
|
||||||
|
manifest.customModules.push(customModule);
|
||||||
|
} else {
|
||||||
|
// Update existing entry
|
||||||
|
manifest.customModules[existingIndex] = customModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.update(bmadDir, { customModules: manifest.customModules });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a custom module from the manifest
|
||||||
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
* @param {string} moduleId - Module ID to remove
|
||||||
|
*/
|
||||||
|
async removeCustomModule(bmadDir, moduleId) {
|
||||||
|
const manifest = await this.read(bmadDir);
|
||||||
|
if (!manifest || !manifest.customModules) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = manifest.customModules.findIndex((m) => m.id === moduleId);
|
||||||
|
if (index !== -1) {
|
||||||
|
manifest.customModules.splice(index, 1);
|
||||||
|
await this.update(bmadDir, { customModules: manifest.customModules });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Manifest };
|
module.exports = { Manifest };
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const fs = require('fs-extra');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
const { FileOps } = require('../../../lib/file-ops');
|
const { FileOps } = require('../../../lib/file-ops');
|
||||||
|
const { XmlHandler } = require('../../../lib/xml-handler');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for custom content (custom.yaml)
|
* Handler for custom content (custom.yaml)
|
||||||
|
|
@ -11,6 +12,7 @@ const { FileOps } = require('../../../lib/file-ops');
|
||||||
class CustomHandler {
|
class CustomHandler {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.fileOps = new FileOps();
|
this.fileOps = new FileOps();
|
||||||
|
this.xmlHandler = new XmlHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -52,6 +54,12 @@ class CustomHandler {
|
||||||
} else if (entry.name === 'custom.yaml') {
|
} else if (entry.name === 'custom.yaml') {
|
||||||
// Found a custom.yaml file
|
// Found a custom.yaml file
|
||||||
customPaths.push(fullPath);
|
customPaths.push(fullPath);
|
||||||
|
} else if (
|
||||||
|
entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory)
|
||||||
|
// Skip if it's in src/modules (those are standard modules)
|
||||||
|
!fullPath.includes(path.join('src', 'modules'))
|
||||||
|
) {
|
||||||
|
customPaths.push(fullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -66,40 +74,44 @@ class CustomHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get custom content info from a custom.yaml file
|
* Get custom content info from a custom.yaml or module.yaml file
|
||||||
* @param {string} customYamlPath - Path to custom.yaml file
|
* @param {string} configPath - Path to config file
|
||||||
* @param {string} projectRoot - Project root directory for calculating relative paths
|
* @param {string} projectRoot - Project root directory for calculating relative paths
|
||||||
* @returns {Object|null} Custom content info
|
* @returns {Object|null} Custom content info
|
||||||
*/
|
*/
|
||||||
async getCustomInfo(customYamlPath, projectRoot = null) {
|
async getCustomInfo(configPath, projectRoot = null) {
|
||||||
try {
|
try {
|
||||||
const configContent = await fs.readFile(customYamlPath, 'utf8');
|
const configContent = await fs.readFile(configPath, 'utf8');
|
||||||
|
|
||||||
// Try to parse YAML with error handling
|
// Try to parse YAML with error handling
|
||||||
let config;
|
let config;
|
||||||
try {
|
try {
|
||||||
config = yaml.load(configContent);
|
config = yaml.load(configContent);
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.warn(chalk.yellow(`Warning: YAML parse error in ${customYamlPath}:`, parseError.message));
|
console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customDir = path.dirname(customYamlPath);
|
// Check if this is an module.yaml (module) or custom.yaml (custom content)
|
||||||
|
const isInstallConfig = configPath.endsWith('module.yaml');
|
||||||
|
const configDir = path.dirname(configPath);
|
||||||
|
|
||||||
// Use provided projectRoot or fall back to process.cwd()
|
// Use provided projectRoot or fall back to process.cwd()
|
||||||
const basePath = projectRoot || process.cwd();
|
const basePath = projectRoot || process.cwd();
|
||||||
const relativePath = path.relative(basePath, customDir);
|
const relativePath = path.relative(basePath, configDir);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: config.code || path.basename(customDir),
|
id: config.code || 'unknown-code',
|
||||||
name: config.name || `Custom: ${path.basename(customDir)}`,
|
name: config.name,
|
||||||
description: config.description || 'Custom agents and workflows',
|
description: config.description || '',
|
||||||
path: customDir,
|
path: configDir,
|
||||||
relativePath: relativePath,
|
relativePath: relativePath,
|
||||||
defaultSelected: config.default_selected === true,
|
defaultSelected: config.default_selected === true,
|
||||||
config: config,
|
config: config,
|
||||||
|
isInstallConfig: isInstallConfig, // Track which type this is
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(chalk.yellow(`Warning: Failed to read ${customYamlPath}:`, error.message));
|
console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,10 +143,10 @@ class CustomHandler {
|
||||||
await fs.ensureDir(bmadAgentsDir);
|
await fs.ensureDir(bmadAgentsDir);
|
||||||
await fs.ensureDir(bmadWorkflowsDir);
|
await fs.ensureDir(bmadWorkflowsDir);
|
||||||
|
|
||||||
// Process agents - copy entire agents directory structure
|
// Process agents - compile and copy agents
|
||||||
const agentsDir = path.join(customPath, 'agents');
|
const agentsDir = path.join(customPath, 'agents');
|
||||||
if (await fs.pathExists(agentsDir)) {
|
if (await fs.pathExists(agentsDir)) {
|
||||||
await this.copyDirectory(agentsDir, bmadAgentsDir, results, fileTrackingCallback, config);
|
await this.compileAndCopyAgents(agentsDir, bmadAgentsDir, bmadDir, config, fileTrackingCallback, results);
|
||||||
|
|
||||||
// Count agent files
|
// Count agent files
|
||||||
const agentFiles = await this.findFilesRecursively(agentsDir, ['.agent.yaml', '.md']);
|
const agentFiles = await this.findFilesRecursively(agentsDir, ['.agent.yaml', '.md']);
|
||||||
|
|
@ -271,6 +283,114 @@ class CustomHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile .agent.yaml files to .md format and handle sidecars
|
||||||
|
* @param {string} sourceAgentsPath - Source agents directory
|
||||||
|
* @param {string} targetAgentsPath - Target agents directory
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @param {Object} config - Configuration for placeholder replacement
|
||||||
|
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
||||||
|
* @param {Object} results - Results object to update
|
||||||
|
*/
|
||||||
|
async compileAndCopyAgents(sourceAgentsPath, targetAgentsPath, bmadDir, config, fileTrackingCallback, results) {
|
||||||
|
// Get all .agent.yaml files recursively
|
||||||
|
const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);
|
||||||
|
|
||||||
|
for (const agentFile of agentFiles) {
|
||||||
|
const relativePath = path.relative(sourceAgentsPath, agentFile);
|
||||||
|
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
|
||||||
|
|
||||||
|
await fs.ensureDir(targetDir);
|
||||||
|
|
||||||
|
const agentName = path.basename(agentFile, '.agent.yaml');
|
||||||
|
const targetMdPath = path.join(targetDir, `${agentName}.md`);
|
||||||
|
// Use the actual bmadDir if available (for when installing to temp dir)
|
||||||
|
const actualBmadDir = config._bmadDir || bmadDir;
|
||||||
|
const customizePath = path.join(actualBmadDir, '_cfg', 'agents', `custom-${agentName}.customize.yaml`);
|
||||||
|
|
||||||
|
// Read and compile the YAML
|
||||||
|
try {
|
||||||
|
const yamlContent = await fs.readFile(agentFile, 'utf8');
|
||||||
|
const { compileAgent } = require('../../../lib/agent/compiler');
|
||||||
|
|
||||||
|
// Create customize template if it doesn't exist
|
||||||
|
if (!(await fs.pathExists(customizePath))) {
|
||||||
|
const { getSourcePath } = require('../../../lib/project-root');
|
||||||
|
const genericTemplatePath = getSourcePath('utility', 'templates', 'agent.customize.template.yaml');
|
||||||
|
if (await fs.pathExists(genericTemplatePath)) {
|
||||||
|
// Copy with placeholder replacement
|
||||||
|
let templateContent = await fs.readFile(genericTemplatePath, 'utf8');
|
||||||
|
templateContent = templateContent.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
|
||||||
|
await fs.writeFile(customizePath, templateContent, 'utf8');
|
||||||
|
console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile the agent
|
||||||
|
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config });
|
||||||
|
|
||||||
|
// Replace placeholders in the compiled content
|
||||||
|
let processedXml = xml;
|
||||||
|
processedXml = processedXml.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
|
||||||
|
processedXml = processedXml.replaceAll('{user_name}', config.user_name || 'User');
|
||||||
|
processedXml = processedXml.replaceAll('{communication_language}', config.communication_language || 'English');
|
||||||
|
processedXml = processedXml.replaceAll('{output_folder}', config.output_folder || 'docs');
|
||||||
|
|
||||||
|
// Write the compiled MD file
|
||||||
|
await fs.writeFile(targetMdPath, processedXml, 'utf8');
|
||||||
|
|
||||||
|
// Check if agent has sidecar
|
||||||
|
let hasSidecar = false;
|
||||||
|
try {
|
||||||
|
const yamlLib = require('yaml');
|
||||||
|
const agentYaml = yamlLib.parse(yamlContent);
|
||||||
|
hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
|
||||||
|
} catch {
|
||||||
|
// Continue without sidecar processing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy sidecar files if agent has hasSidecar flag
|
||||||
|
if (hasSidecar && config.agent_sidecar_folder) {
|
||||||
|
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
|
||||||
|
|
||||||
|
// Resolve agent sidecar folder path
|
||||||
|
const projectDir = path.dirname(bmadDir);
|
||||||
|
const resolvedSidecarFolder = config.agent_sidecar_folder
|
||||||
|
.replaceAll('{project-root}', projectDir)
|
||||||
|
.replaceAll('{bmad_folder}', path.basename(bmadDir));
|
||||||
|
|
||||||
|
// Create sidecar directory for this agent
|
||||||
|
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
|
||||||
|
await fs.ensureDir(agentSidecarDir);
|
||||||
|
|
||||||
|
// Copy sidecar files
|
||||||
|
const sidecarResult = copyAgentSidecarFiles(path.dirname(agentFile), agentSidecarDir, agentFile);
|
||||||
|
|
||||||
|
if (sidecarResult.copied.length > 0) {
|
||||||
|
console.log(chalk.dim(` Copied ${sidecarResult.copied.length} sidecar file(s) to: ${agentSidecarDir}`));
|
||||||
|
}
|
||||||
|
if (sidecarResult.preserved.length > 0) {
|
||||||
|
console.log(chalk.dim(` Preserved ${sidecarResult.preserved.length} existing sidecar file(s)`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the file
|
||||||
|
if (fileTrackingCallback) {
|
||||||
|
fileTrackingCallback(targetMdPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
chalk.dim(
|
||||||
|
` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
|
||||||
|
results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { CustomHandler };
|
module.exports = { CustomHandler };
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,12 @@ const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/p
|
||||||
* await manager.install('core-module', '/path/to/bmad');
|
* await manager.install('core-module', '/path/to/bmad');
|
||||||
*/
|
*/
|
||||||
class ModuleManager {
|
class ModuleManager {
|
||||||
constructor() {
|
constructor(options = {}) {
|
||||||
// Path to source modules directory
|
// Path to source modules directory
|
||||||
this.modulesSourcePath = getSourcePath('modules');
|
this.modulesSourcePath = getSourcePath('modules');
|
||||||
this.xmlHandler = new XmlHandler();
|
this.xmlHandler = new XmlHandler();
|
||||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
||||||
|
this.scanProjectForModules = options.scanProjectForModules !== false; // Default to true for backward compatibility
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -106,7 +107,7 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all modules in the project by searching for install-config.yaml files
|
* Find all modules in the project by searching for module.yaml files
|
||||||
* @returns {Array} List of module paths
|
* @returns {Array} List of module paths
|
||||||
*/
|
*/
|
||||||
async findModulesInProject() {
|
async findModulesInProject() {
|
||||||
|
|
@ -143,12 +144,14 @@ class ModuleManager {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this directory contains a module (install-config.yaml OR custom.yaml)
|
// Check if this directory contains a module (module.yaml OR custom.yaml)
|
||||||
const installerConfigPath = path.join(fullPath, '_module-installer', 'install-config.yaml');
|
const moduleConfigPath = path.join(fullPath, 'module.yaml');
|
||||||
|
const installerConfigPath = path.join(fullPath, '_module-installer', 'module.yaml');
|
||||||
const customConfigPath = path.join(fullPath, '_module-installer', 'custom.yaml');
|
const customConfigPath = path.join(fullPath, '_module-installer', 'custom.yaml');
|
||||||
const rootCustomConfigPath = path.join(fullPath, 'custom.yaml');
|
const rootCustomConfigPath = path.join(fullPath, 'custom.yaml');
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
(await fs.pathExists(moduleConfigPath)) ||
|
||||||
(await fs.pathExists(installerConfigPath)) ||
|
(await fs.pathExists(installerConfigPath)) ||
|
||||||
(await fs.pathExists(customConfigPath)) ||
|
(await fs.pathExists(customConfigPath)) ||
|
||||||
(await fs.pathExists(rootCustomConfigPath))
|
(await fs.pathExists(rootCustomConfigPath))
|
||||||
|
|
@ -175,10 +178,11 @@ class ModuleManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all available modules (excluding core which is always installed)
|
* List all available modules (excluding core which is always installed)
|
||||||
* @returns {Array} List of available modules with metadata
|
* @returns {Object} Object with modules array and customModules array
|
||||||
*/
|
*/
|
||||||
async listAvailable() {
|
async listAvailable() {
|
||||||
const modules = [];
|
const modules = [];
|
||||||
|
const customModules = [];
|
||||||
|
|
||||||
// First, scan src/modules (the standard location)
|
// First, scan src/modules (the standard location)
|
||||||
if (await fs.pathExists(this.modulesSourcePath)) {
|
if (await fs.pathExists(this.modulesSourcePath)) {
|
||||||
|
|
@ -187,12 +191,17 @@ class ModuleManager {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
const modulePath = path.join(this.modulesSourcePath, entry.name);
|
const modulePath = path.join(this.modulesSourcePath, entry.name);
|
||||||
// Check for module structure (install-config.yaml OR custom.yaml)
|
// Check for module structure (module.yaml OR custom.yaml)
|
||||||
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml');
|
const moduleConfigPath = path.join(modulePath, 'module.yaml');
|
||||||
|
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
|
||||||
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
|
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
|
||||||
|
|
||||||
// Skip if this doesn't look like a module
|
// Skip if this doesn't look like a module
|
||||||
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(customConfigPath))) {
|
if (
|
||||||
|
!(await fs.pathExists(moduleConfigPath)) &&
|
||||||
|
!(await fs.pathExists(installerConfigPath)) &&
|
||||||
|
!(await fs.pathExists(customConfigPath))
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,7 +218,8 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then, find all other modules in the project
|
// Then, find all other modules in the project (only if scanning is enabled)
|
||||||
|
if (this.scanProjectForModules) {
|
||||||
const otherModulePaths = await this.findModulesInProject();
|
const otherModulePaths = await this.findModulesInProject();
|
||||||
for (const modulePath of otherModulePaths) {
|
for (const modulePath of otherModulePaths) {
|
||||||
const moduleName = path.basename(modulePath);
|
const moduleName = path.basename(modulePath);
|
||||||
|
|
@ -221,13 +231,37 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleInfo = await this.getModuleInfo(modulePath, moduleName, relativePath);
|
const moduleInfo = await this.getModuleInfo(modulePath, moduleName, relativePath);
|
||||||
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id)) {
|
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
|
||||||
// Avoid duplicates - skip if we already have this module ID
|
// Avoid duplicates - skip if we already have this module ID
|
||||||
|
if (moduleInfo.isCustom) {
|
||||||
|
customModules.push(moduleInfo);
|
||||||
|
} else {
|
||||||
modules.push(moduleInfo);
|
modules.push(moduleInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return modules;
|
// Also check for cached custom modules in _cfg/custom/
|
||||||
|
if (this.bmadDir) {
|
||||||
|
const customCacheDir = path.join(this.bmadDir, '_cfg', 'custom');
|
||||||
|
if (await fs.pathExists(customCacheDir)) {
|
||||||
|
const cacheEntries = await fs.readdir(customCacheDir, { withFileTypes: true });
|
||||||
|
for (const entry of cacheEntries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const cachePath = path.join(customCacheDir, entry.name);
|
||||||
|
const moduleInfo = await this.getModuleInfo(cachePath, entry.name, '_cfg/custom');
|
||||||
|
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
|
||||||
|
moduleInfo.isCustom = true;
|
||||||
|
moduleInfo.fromCache = true;
|
||||||
|
customModules.push(moduleInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modules, customModules };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -238,13 +272,16 @@ class ModuleManager {
|
||||||
* @returns {Object|null} Module info or null if not a valid module
|
* @returns {Object|null} Module info or null if not a valid module
|
||||||
*/
|
*/
|
||||||
async getModuleInfo(modulePath, defaultName, sourceDescription) {
|
async getModuleInfo(modulePath, defaultName, sourceDescription) {
|
||||||
// Check for module structure (install-config.yaml OR custom.yaml)
|
// Check for module structure (module.yaml OR custom.yaml)
|
||||||
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml');
|
const moduleConfigPath = path.join(modulePath, 'module.yaml');
|
||||||
|
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
|
||||||
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
|
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
|
||||||
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
|
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
|
||||||
let configPath = null;
|
let configPath = null;
|
||||||
|
|
||||||
if (await fs.pathExists(installerConfigPath)) {
|
if (await fs.pathExists(moduleConfigPath)) {
|
||||||
|
configPath = moduleConfigPath;
|
||||||
|
} else if (await fs.pathExists(installerConfigPath)) {
|
||||||
configPath = installerConfigPath;
|
configPath = installerConfigPath;
|
||||||
} else if (await fs.pathExists(customConfigPath)) {
|
} else if (await fs.pathExists(customConfigPath)) {
|
||||||
configPath = customConfigPath;
|
configPath = customConfigPath;
|
||||||
|
|
@ -305,10 +342,11 @@ class ModuleManager {
|
||||||
// First, check src/modules
|
// First, check src/modules
|
||||||
const srcModulePath = path.join(this.modulesSourcePath, moduleName);
|
const srcModulePath = path.join(this.modulesSourcePath, moduleName);
|
||||||
if (await fs.pathExists(srcModulePath)) {
|
if (await fs.pathExists(srcModulePath)) {
|
||||||
// Check if this looks like a module (has install-config.yaml)
|
// Check if this looks like a module (has module.yaml)
|
||||||
const installerConfigPath = path.join(srcModulePath, '_module-installer', 'install-config.yaml');
|
const moduleConfigPath = path.join(srcModulePath, 'module.yaml');
|
||||||
|
const installerConfigPath = path.join(srcModulePath, '_module-installer', 'module.yaml');
|
||||||
|
|
||||||
if (await fs.pathExists(installerConfigPath)) {
|
if ((await fs.pathExists(moduleConfigPath)) || (await fs.pathExists(installerConfigPath))) {
|
||||||
return srcModulePath;
|
return srcModulePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -330,12 +368,15 @@ class ModuleManager {
|
||||||
// Also check by module ID (not just folder name)
|
// Also check by module ID (not just folder name)
|
||||||
// Need to read configs to match by ID
|
// Need to read configs to match by ID
|
||||||
for (const modulePath of allModulePaths) {
|
for (const modulePath of allModulePaths) {
|
||||||
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml');
|
const moduleConfigPath = path.join(modulePath, 'module.yaml');
|
||||||
|
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
|
||||||
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
|
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
|
||||||
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
|
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
|
||||||
|
|
||||||
let configPath = null;
|
let configPath = null;
|
||||||
if (await fs.pathExists(installerConfigPath)) {
|
if (await fs.pathExists(moduleConfigPath)) {
|
||||||
|
configPath = moduleConfigPath;
|
||||||
|
} else if (await fs.pathExists(installerConfigPath)) {
|
||||||
configPath = installerConfigPath;
|
configPath = installerConfigPath;
|
||||||
} else if (await fs.pathExists(customConfigPath)) {
|
} else if (await fs.pathExists(customConfigPath)) {
|
||||||
configPath = customConfigPath;
|
configPath = customConfigPath;
|
||||||
|
|
@ -576,7 +617,7 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip _module-installer directory - it's only needed at install time
|
// Skip _module-installer directory - it's only needed at install time
|
||||||
if (file.startsWith('_module-installer/')) {
|
if (file.startsWith('_module-installer/') || file === 'module.yaml') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -812,8 +853,13 @@ class ModuleManager {
|
||||||
// Compile with customizations if any
|
// Compile with customizations if any
|
||||||
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config: this.coreConfig });
|
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config: this.coreConfig });
|
||||||
|
|
||||||
// Write the compiled MD file
|
// Replace {bmad_folder} placeholder if needed
|
||||||
|
if (xml.includes('{bmad_folder}') && this.bmadFolderName) {
|
||||||
|
const processedXml = xml.replaceAll('{bmad_folder}', this.bmadFolderName);
|
||||||
|
await fs.writeFile(targetMdPath, processedXml, 'utf8');
|
||||||
|
} else {
|
||||||
await fs.writeFile(targetMdPath, xml, 'utf8');
|
await fs.writeFile(targetMdPath, xml, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
// Copy sidecar files if agent has hasSidecar flag
|
// Copy sidecar files if agent has hasSidecar flag
|
||||||
if (hasSidecar) {
|
if (hasSidecar) {
|
||||||
|
|
|
||||||
|
|
@ -445,17 +445,9 @@ function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = ''
|
||||||
// Parse YAML
|
// Parse YAML
|
||||||
const agentYaml = yaml.parse(yamlContent);
|
const agentYaml = yaml.parse(yamlContent);
|
||||||
|
|
||||||
// Inject custom agent name into metadata.name if provided
|
// Note: agentName parameter is for UI display only, not for modifying the YAML
|
||||||
// This is the user's chosen persona name (e.g., "Fred" instead of "Inkwell Von Comitizen")
|
// The persona name (metadata.name) should always come from the YAML file
|
||||||
if (agentName && agentYaml.agent && agentYaml.agent.metadata) {
|
// We should NEVER modify metadata.name as it's part of the agent's identity
|
||||||
// Convert kebab-case to title case for the name field
|
|
||||||
// e.g., "fred-commit-poet" → "Fred Commit Poet"
|
|
||||||
const titleCaseName = agentName
|
|
||||||
.split('-')
|
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
||||||
.join(' ');
|
|
||||||
agentYaml.agent.metadata.name = titleCaseName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract install_config
|
// Extract install_config
|
||||||
const installConfig = extractInstallConfig(agentYaml);
|
const installConfig = extractInstallConfig(agentYaml);
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,8 @@ function installAgent(agentInfo, answers, targetPath, options = {}) {
|
||||||
const { xml, metadata, processedYaml } = compileAgent(fs.readFileSync(agentInfo.yamlFile, 'utf8'), answers);
|
const { xml, metadata, processedYaml } = compileAgent(fs.readFileSync(agentInfo.yamlFile, 'utf8'), answers);
|
||||||
|
|
||||||
// Determine target agent folder name
|
// Determine target agent folder name
|
||||||
const agentFolderName = metadata.name ? metadata.name.toLowerCase().replaceAll(/\s+/g, '-') : agentInfo.name;
|
// Use the folder name from agentInfo, NOT the persona name from metadata
|
||||||
|
const agentFolderName = agentInfo.name;
|
||||||
|
|
||||||
const agentTargetDir = path.join(targetPath, agentFolderName);
|
const agentTargetDir = path.join(targetPath, agentFolderName);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const boxen = require('boxen');
|
||||||
const wrapAnsi = require('wrap-ansi');
|
const wrapAnsi = require('wrap-ansi');
|
||||||
const figlet = require('figlet');
|
const figlet = require('figlet');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
const os = require('node:os');
|
||||||
|
|
||||||
const CLIUtils = {
|
const CLIUtils = {
|
||||||
/**
|
/**
|
||||||
|
|
@ -84,8 +85,8 @@ const CLIUtils = {
|
||||||
/**
|
/**
|
||||||
* Display module configuration header
|
* Display module configuration header
|
||||||
* @param {string} moduleName - Module name (fallback if no custom header)
|
* @param {string} moduleName - Module name (fallback if no custom header)
|
||||||
* @param {string} header - Custom header from install-config.yaml
|
* @param {string} header - Custom header from module.yaml
|
||||||
* @param {string} subheader - Custom subheader from install-config.yaml
|
* @param {string} subheader - Custom subheader from module.yaml
|
||||||
*/
|
*/
|
||||||
displayModuleConfigHeader(moduleName, header = null, subheader = null) {
|
displayModuleConfigHeader(moduleName, header = null, subheader = null) {
|
||||||
// Simple blue banner with custom header/subheader if provided
|
// Simple blue banner with custom header/subheader if provided
|
||||||
|
|
@ -100,8 +101,8 @@ const CLIUtils = {
|
||||||
/**
|
/**
|
||||||
* Display module with no custom configuration
|
* Display module with no custom configuration
|
||||||
* @param {string} moduleName - Module name (fallback if no custom header)
|
* @param {string} moduleName - Module name (fallback if no custom header)
|
||||||
* @param {string} header - Custom header from install-config.yaml
|
* @param {string} header - Custom header from module.yaml
|
||||||
* @param {string} subheader - Custom subheader from install-config.yaml
|
* @param {string} subheader - Custom subheader from module.yaml
|
||||||
*/
|
*/
|
||||||
displayModuleNoConfig(moduleName, header = null, subheader = null) {
|
displayModuleNoConfig(moduleName, header = null, subheader = null) {
|
||||||
// Show full banner with header/subheader, just like modules with config
|
// Show full banner with header/subheader, just like modules with config
|
||||||
|
|
@ -205,6 +206,22 @@ const CLIUtils = {
|
||||||
// No longer clear screen or show boxes - just a simple completion message
|
// No longer clear screen or show boxes - just a simple completion message
|
||||||
// This is deprecated but kept for backwards compatibility
|
// This is deprecated but kept for backwards compatibility
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand path with ~ expansion
|
||||||
|
* @param {string} inputPath - Path to expand
|
||||||
|
* @returns {string} Expanded path
|
||||||
|
*/
|
||||||
|
expandPath(inputPath) {
|
||||||
|
if (!inputPath) return inputPath;
|
||||||
|
|
||||||
|
// Expand ~ to home directory
|
||||||
|
if (inputPath.startsWith('~')) {
|
||||||
|
return path.join(os.homedir(), inputPath.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputPath;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { CLIUtils };
|
module.exports = { CLIUtils };
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,6 @@ class UI {
|
||||||
await installer.handleLegacyV4Migration(confirmedDirectory, legacyV4);
|
await installer.handleLegacyV4Migration(confirmedDirectory, legacyV4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt for custom content location (separate from installation directory)
|
|
||||||
const customContentConfig = await this.promptCustomContentLocation();
|
|
||||||
|
|
||||||
// Check if there's an existing BMAD installation
|
// Check if there's an existing BMAD installation
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
@ -62,6 +59,17 @@ class UI {
|
||||||
const bmadDir = await installer.findBmadDir(confirmedDirectory);
|
const bmadDir = await installer.findBmadDir(confirmedDirectory);
|
||||||
const hasExistingInstall = await fs.pathExists(bmadDir);
|
const hasExistingInstall = await fs.pathExists(bmadDir);
|
||||||
|
|
||||||
|
// Always ask for custom content, but we'll handle it differently for new installs
|
||||||
|
let customContentConfig = { hasCustomContent: false };
|
||||||
|
if (hasExistingInstall) {
|
||||||
|
// Existing installation - prompt to add/update custom content
|
||||||
|
customContentConfig = await this.promptCustomContentForExisting();
|
||||||
|
} else {
|
||||||
|
// New installation - we'll prompt after creating the directory structure
|
||||||
|
// For now, set a flag to indicate we should ask later
|
||||||
|
customContentConfig._shouldAsk = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Track action type (only set if there's an existing installation)
|
// Track action type (only set if there's an existing installation)
|
||||||
let actionType;
|
let actionType;
|
||||||
|
|
||||||
|
|
@ -88,12 +96,11 @@ class UI {
|
||||||
|
|
||||||
// Handle quick update separately
|
// Handle quick update separately
|
||||||
if (actionType === 'quick-update') {
|
if (actionType === 'quick-update') {
|
||||||
// Even for quick update, ask about custom content
|
// Quick update doesn't install custom content - just updates existing modules
|
||||||
const customContentConfig = await this.promptCustomContentLocation();
|
|
||||||
return {
|
return {
|
||||||
actionType: 'quick-update',
|
actionType: 'quick-update',
|
||||||
directory: confirmedDirectory,
|
directory: confirmedDirectory,
|
||||||
customContent: customContentConfig,
|
customContent: { hasCustomContent: false },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,6 +130,64 @@ class UI {
|
||||||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
||||||
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
|
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
|
||||||
|
|
||||||
|
// For new installations, create the directory structure first so we can cache custom content
|
||||||
|
if (!hasExistingInstall && customContentConfig._shouldAsk) {
|
||||||
|
// Create the bmad directory based on core config
|
||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const bmadFolderName = coreConfig.bmad_folder || 'bmad';
|
||||||
|
const bmadDir = path.join(confirmedDirectory, bmadFolderName);
|
||||||
|
|
||||||
|
await fs.ensureDir(bmadDir);
|
||||||
|
await fs.ensureDir(path.join(bmadDir, '_cfg'));
|
||||||
|
await fs.ensureDir(path.join(bmadDir, '_cfg', 'custom'));
|
||||||
|
|
||||||
|
// Now prompt for custom content
|
||||||
|
customContentConfig = await this.promptCustomContentLocation();
|
||||||
|
|
||||||
|
// If custom content found, cache it
|
||||||
|
if (customContentConfig.hasCustomContent) {
|
||||||
|
const { CustomModuleCache } = require('../installers/lib/core/custom-module-cache');
|
||||||
|
const cache = new CustomModuleCache(bmadDir);
|
||||||
|
|
||||||
|
const { CustomHandler } = require('../installers/lib/custom/handler');
|
||||||
|
const customHandler = new CustomHandler();
|
||||||
|
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
|
||||||
|
|
||||||
|
for (const customFile of customFiles) {
|
||||||
|
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||||
|
if (customInfo && customInfo.id) {
|
||||||
|
// Cache the module source
|
||||||
|
await cache.cacheModule(customInfo.id, customInfo.path, {
|
||||||
|
name: customInfo.name,
|
||||||
|
type: 'custom',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(chalk.dim(` Cached ${customInfo.name} to _cfg/custom/${customInfo.id}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config to use cached modules
|
||||||
|
customContentConfig.cachedModules = [];
|
||||||
|
for (const customFile of customFiles) {
|
||||||
|
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||||
|
if (customInfo && customInfo.id) {
|
||||||
|
customContentConfig.cachedModules.push({
|
||||||
|
id: customInfo.id,
|
||||||
|
cachePath: path.join(bmadDir, '_cfg', 'custom', customInfo.id),
|
||||||
|
// Store relative path from cache for the manifest
|
||||||
|
relativePath: path.join('_cfg', 'custom', customInfo.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.green(`✓ Cached ${customFiles.length} custom module(s)`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the flag
|
||||||
|
delete customContentConfig._shouldAsk;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip module selection during update/reinstall - keep existing modules
|
// Skip module selection during update/reinstall - keep existing modules
|
||||||
let selectedModules;
|
let selectedModules;
|
||||||
if (actionType === 'update' || actionType === 'reinstall') {
|
if (actionType === 'update' || actionType === 'reinstall') {
|
||||||
|
|
@ -136,15 +201,46 @@ class UI {
|
||||||
|
|
||||||
// Check which custom content items were selected
|
// Check which custom content items were selected
|
||||||
const selectedCustomContent = selectedModules.filter((mod) => mod.startsWith('__CUSTOM_CONTENT__'));
|
const selectedCustomContent = selectedModules.filter((mod) => mod.startsWith('__CUSTOM_CONTENT__'));
|
||||||
if (selectedCustomContent.length > 0) {
|
|
||||||
|
// For cached modules (new installs), check if any cached modules were selected
|
||||||
|
let selectedCachedModules = [];
|
||||||
|
if (customContentConfig.cachedModules) {
|
||||||
|
selectedCachedModules = selectedModules.filter(
|
||||||
|
(mod) => !mod.startsWith('__CUSTOM_CONTENT__') && customContentConfig.cachedModules.some((cm) => cm.id === mod),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCustomContent.length > 0 || selectedCachedModules.length > 0) {
|
||||||
customContentConfig.selected = true;
|
customContentConfig.selected = true;
|
||||||
|
|
||||||
|
// Handle directory-based custom content (existing installs)
|
||||||
|
if (selectedCustomContent.length > 0) {
|
||||||
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
|
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
|
||||||
// Filter out custom content markers since they're not real modules
|
// Convert custom content to module IDs for installation
|
||||||
selectedModules = selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__'));
|
const customContentModuleIds = [];
|
||||||
|
const { CustomHandler } = require('../installers/lib/custom/handler');
|
||||||
|
const customHandler = new CustomHandler();
|
||||||
|
for (const customFile of customContentConfig.selectedFiles) {
|
||||||
|
// Get the module info to extract the ID
|
||||||
|
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||||
|
if (customInfo) {
|
||||||
|
customContentModuleIds.push(customInfo.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filter out custom content markers and add module IDs
|
||||||
|
selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
// For cached modules, they're already module IDs, just mark as selected
|
||||||
|
if (selectedCachedModules.length > 0) {
|
||||||
|
customContentConfig.selectedCachedModules = selectedCachedModules;
|
||||||
|
// No need to filter since they're already proper module IDs
|
||||||
|
}
|
||||||
} else if (customContentConfig.hasCustomContent) {
|
} else if (customContentConfig.hasCustomContent) {
|
||||||
// User provided custom content but didn't select any
|
// User provided custom content but didn't select any
|
||||||
customContentConfig.selected = false;
|
customContentConfig.selected = false;
|
||||||
customContentConfig.selectedFiles = [];
|
customContentConfig.selectedFiles = [];
|
||||||
|
customContentConfig.selectedCachedModules = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -511,12 +607,66 @@ class UI {
|
||||||
const moduleChoices = [];
|
const moduleChoices = [];
|
||||||
const isNewInstallation = installedModuleIds.size === 0;
|
const isNewInstallation = installedModuleIds.size === 0;
|
||||||
|
|
||||||
// Add custom content items first if found
|
const customContentItems = [];
|
||||||
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
|
const hasCustomContentItems = false;
|
||||||
// Add separator before custom content
|
|
||||||
moduleChoices.push(new inquirer.Separator('── Custom Content ──'));
|
|
||||||
|
|
||||||
// Get the custom content info to display proper names
|
// Add custom content items
|
||||||
|
if (customContentConfig && customContentConfig.hasCustomContent) {
|
||||||
|
if (customContentConfig.cachedModules) {
|
||||||
|
// New installation - show cached modules
|
||||||
|
for (const cachedModule of customContentConfig.cachedModules) {
|
||||||
|
// Get the module info from cache
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
|
// Try multiple possible config file locations
|
||||||
|
const possibleConfigPaths = [
|
||||||
|
path.join(cachedModule.cachePath, 'module.yaml'),
|
||||||
|
path.join(cachedModule.cachePath, 'custom.yaml'),
|
||||||
|
path.join(cachedModule.cachePath, '_module-installer', 'module.yaml'),
|
||||||
|
path.join(cachedModule.cachePath, '_module-installer', 'custom.yaml'),
|
||||||
|
];
|
||||||
|
|
||||||
|
let moduleData = null;
|
||||||
|
let foundPath = null;
|
||||||
|
|
||||||
|
for (const configPath of possibleConfigPaths) {
|
||||||
|
if (await fs.pathExists(configPath)) {
|
||||||
|
try {
|
||||||
|
const yamlContent = await fs.readFile(configPath, 'utf8');
|
||||||
|
moduleData = yaml.load(yamlContent);
|
||||||
|
foundPath = configPath;
|
||||||
|
break;
|
||||||
|
} catch {
|
||||||
|
// Continue to next path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleData) {
|
||||||
|
// Use the name from the custom info if we have it
|
||||||
|
const moduleName = cachedModule.name || moduleData.name || cachedModule.id;
|
||||||
|
|
||||||
|
customContentItems.push({
|
||||||
|
name: `${chalk.cyan('✓')} ${moduleName} ${chalk.gray('(cached)')}`,
|
||||||
|
value: cachedModule.id, // Use module ID directly
|
||||||
|
checked: true, // Default to selected
|
||||||
|
cached: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Debug: show what paths we tried to check
|
||||||
|
console.log(chalk.dim(`DEBUG: No module config found for ${cachedModule.id}`));
|
||||||
|
console.log(
|
||||||
|
chalk.dim(
|
||||||
|
`DEBUG: Tried paths:`,
|
||||||
|
possibleConfigPaths.map((p) => p.replace(cachedModule.cachePath, '.')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log(chalk.dim(`DEBUG: cachedModule:`, JSON.stringify(cachedModule, null, 2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (customContentConfig.customPath) {
|
||||||
|
// Existing installation - show from directory
|
||||||
const { CustomHandler } = require('../installers/lib/custom/handler');
|
const { CustomHandler } = require('../installers/lib/custom/handler');
|
||||||
const customHandler = new CustomHandler();
|
const customHandler = new CustomHandler();
|
||||||
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
|
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
|
||||||
|
|
@ -524,30 +674,68 @@ class UI {
|
||||||
for (const customFile of customFiles) {
|
for (const customFile of customFiles) {
|
||||||
const customInfo = await customHandler.getCustomInfo(customFile);
|
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||||
if (customInfo) {
|
if (customInfo) {
|
||||||
moduleChoices.push({
|
customContentItems.push({
|
||||||
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
|
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
|
||||||
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
|
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
|
||||||
checked: true, // Default to selected since user chose to provide custom content
|
checked: true, // Default to selected since user chose to provide custom content
|
||||||
|
path: customInfo.path, // Track path to avoid duplicates
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Add separator for official content
|
|
||||||
moduleChoices.push(new inquirer.Separator('── Official Content ──'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add official modules
|
// Add official modules
|
||||||
const { ModuleManager } = require('../installers/lib/modules/manager');
|
const { ModuleManager } = require('../installers/lib/modules/manager');
|
||||||
const moduleManager = new ModuleManager();
|
// For new installations, don't scan project yet (will do after custom content is discovered)
|
||||||
const availableModules = await moduleManager.listAvailable();
|
// For existing installations, scan if user selected custom content
|
||||||
|
const shouldScanProject =
|
||||||
|
!isNewInstallation && customContentConfig && customContentConfig.hasCustomContent && customContentConfig.selected;
|
||||||
|
const moduleManager = new ModuleManager({
|
||||||
|
scanProjectForModules: shouldScanProject,
|
||||||
|
});
|
||||||
|
const { modules: availableModules, customModules: customModulesFromProject } = await moduleManager.listAvailable();
|
||||||
|
|
||||||
|
// First, add all items to appropriate sections
|
||||||
|
const allCustomModules = [];
|
||||||
|
|
||||||
|
// Add custom content items from directory
|
||||||
|
allCustomModules.push(...customContentItems);
|
||||||
|
|
||||||
|
// Add custom modules from project scan (if scanning is enabled)
|
||||||
|
for (const mod of customModulesFromProject) {
|
||||||
|
// Skip if this module is already in customContentItems (by path)
|
||||||
|
const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path));
|
||||||
|
|
||||||
|
if (!isDuplicate) {
|
||||||
|
allCustomModules.push({
|
||||||
|
name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(${mod.source})`)}`,
|
||||||
|
value: mod.id,
|
||||||
|
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add separators and modules in correct order
|
||||||
|
if (allCustomModules.length > 0) {
|
||||||
|
// Add separator for custom content, all custom modules, and official content separator
|
||||||
|
moduleChoices.push(
|
||||||
|
new inquirer.Separator('── Custom Content ──'),
|
||||||
|
...allCustomModules,
|
||||||
|
new inquirer.Separator('── Official Content ──'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add official modules (only non-custom ones)
|
||||||
for (const mod of availableModules) {
|
for (const mod of availableModules) {
|
||||||
|
if (!mod.isCustom) {
|
||||||
moduleChoices.push({
|
moduleChoices.push({
|
||||||
name: mod.name,
|
name: mod.name,
|
||||||
value: mod.id,
|
value: mod.id,
|
||||||
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return moduleChoices;
|
return moduleChoices;
|
||||||
}
|
}
|
||||||
|
|
@ -632,7 +820,7 @@ class UI {
|
||||||
*/
|
*/
|
||||||
async promptCustomContentLocation() {
|
async promptCustomContentLocation() {
|
||||||
try {
|
try {
|
||||||
CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents and workflows');
|
CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents, workflows, and modules');
|
||||||
|
|
||||||
const { hasCustomContent } = await inquirer.prompt([
|
const { hasCustomContent } = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
|
|
@ -666,7 +854,7 @@ class UI {
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'directory',
|
name: 'directory',
|
||||||
message: 'Enter the path to your custom content directory:',
|
message: 'Enter directory to search for custom content (will scan subfolders):',
|
||||||
default: process.cwd(), // Use actual current working directory
|
default: process.cwd(), // Use actual current working directory
|
||||||
validate: async (input) => {
|
validate: async (input) => {
|
||||||
if (!input || input.trim() === '') {
|
if (!input || input.trim() === '') {
|
||||||
|
|
@ -699,7 +887,7 @@ class UI {
|
||||||
const customFiles = await customHandler.findCustomContent(expandedPath);
|
const customFiles = await customHandler.findCustomContent(expandedPath);
|
||||||
|
|
||||||
if (customFiles.length === 0) {
|
if (customFiles.length === 0) {
|
||||||
console.log(chalk.yellow(`\nNo custom.yaml files found in ${expandedPath}`));
|
console.log(chalk.yellow(`\nNo custom content found in ${expandedPath}`));
|
||||||
|
|
||||||
const { tryAgain } = await inquirer.prompt([
|
const { tryAgain } = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
|
|
@ -718,7 +906,12 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
customPath = expandedPath;
|
customPath = expandedPath;
|
||||||
console.log(chalk.green(`\n✓ Found ${customFiles.length} custom content file(s)`));
|
console.log(chalk.green(`\n✓ Found ${customFiles.length} custom content item(s):`));
|
||||||
|
for (const file of customFiles) {
|
||||||
|
const relativePath = path.relative(expandedPath, path.dirname(file));
|
||||||
|
const folderName = path.dirname(file).split(path.sep).pop();
|
||||||
|
console.log(chalk.dim(` • ${folderName} ${chalk.gray(`(${relativePath})`)}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { hasCustomContent: true, customPath };
|
return { hasCustomContent: true, customPath };
|
||||||
|
|
@ -1016,6 +1209,144 @@ class UI {
|
||||||
|
|
||||||
return (await fs.pathExists(hookPath)) && (await fs.pathExists(playTtsPath));
|
return (await fs.pathExists(hookPath)) && (await fs.pathExists(playTtsPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt for custom content for existing installations
|
||||||
|
* @returns {Object} Custom content configuration
|
||||||
|
*/
|
||||||
|
async promptCustomContentForExisting() {
|
||||||
|
try {
|
||||||
|
CLIUtils.displaySection('Custom Content', 'Add new custom agents, workflows, or modules to your installation');
|
||||||
|
|
||||||
|
const { hasCustomContent } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'hasCustomContent',
|
||||||
|
message: 'Do you want to add or update custom content?',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
name: 'No, continue with current installation only',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Yes, I have custom content to add or update',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!hasCustomContent) {
|
||||||
|
return { hasCustomContent: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get directory path
|
||||||
|
const { customPath } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'customPath',
|
||||||
|
message: 'Enter directory to search for custom content (will scan subfolders):',
|
||||||
|
default: process.cwd(),
|
||||||
|
validate: async (input) => {
|
||||||
|
if (!input || input.trim() === '') {
|
||||||
|
return 'Please enter a directory path';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize and check if path exists
|
||||||
|
const expandedPath = CLIUtils.expandPath(input.trim());
|
||||||
|
const pathExists = await fs.pathExists(expandedPath);
|
||||||
|
if (!pathExists) {
|
||||||
|
return 'Directory does not exist';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's actually a directory
|
||||||
|
const stats = await fs.stat(expandedPath);
|
||||||
|
if (!stats.isDirectory()) {
|
||||||
|
return 'Path must be a directory';
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
transformer: (input) => {
|
||||||
|
return CLIUtils.expandPath(input);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const resolvedPath = CLIUtils.expandPath(customPath);
|
||||||
|
|
||||||
|
// Find custom content
|
||||||
|
const { CustomHandler } = require('../installers/lib/custom/handler');
|
||||||
|
const customHandler = new CustomHandler();
|
||||||
|
const customFiles = await customHandler.findCustomContent(resolvedPath);
|
||||||
|
|
||||||
|
if (customFiles.length === 0) {
|
||||||
|
console.log(chalk.yellow(`\nNo custom content found in ${resolvedPath}`));
|
||||||
|
|
||||||
|
const { tryDifferent } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'tryDifferent',
|
||||||
|
message: 'Try a different directory?',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (tryDifferent) {
|
||||||
|
return await this.promptCustomContentForExisting();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hasCustomContent: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display found items
|
||||||
|
console.log(chalk.cyan(`\nFound ${customFiles.length} custom content file(s):`));
|
||||||
|
const { CustomHandler: CustomHandler2 } = require('../installers/lib/custom/handler');
|
||||||
|
const customHandler2 = new CustomHandler2();
|
||||||
|
const customContentItems = [];
|
||||||
|
|
||||||
|
for (const customFile of customFiles) {
|
||||||
|
const customInfo = await customHandler2.getCustomInfo(customFile);
|
||||||
|
if (customInfo) {
|
||||||
|
customContentItems.push({
|
||||||
|
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
|
||||||
|
value: `__CUSTOM_CONTENT__${customFile}`,
|
||||||
|
checked: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add option to keep existing custom content
|
||||||
|
console.log(chalk.yellow('\nExisting custom modules will be preserved unless you remove them'));
|
||||||
|
|
||||||
|
const { selectedFiles } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'selectedFiles',
|
||||||
|
message: 'Select custom content to add:',
|
||||||
|
choices: customContentItems,
|
||||||
|
pageSize: 15,
|
||||||
|
validate: (answer) => {
|
||||||
|
if (answer.length === 0) {
|
||||||
|
return 'You must select at least one item';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasCustomContent: true,
|
||||||
|
customPath: resolvedPath,
|
||||||
|
selected: true,
|
||||||
|
selectedFiles: selectedFiles,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(chalk.red('Error configuring custom content:'), error);
|
||||||
|
return { hasCustomContent: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { UI };
|
module.exports = { UI };
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
/**
|
||||||
|
* Migration script to convert relative paths to absolute paths in custom module manifests
|
||||||
|
* This should be run once to update existing installations
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('node:path');
|
||||||
|
const yaml = require('yaml');
|
||||||
|
const chalk = require('chalk');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find BMAD directory in project
|
||||||
|
*/
|
||||||
|
function findBmadDir(projectDir = process.cwd()) {
|
||||||
|
const possibleNames = ['bmad', '.bmad'];
|
||||||
|
|
||||||
|
for (const name of possibleNames) {
|
||||||
|
const bmadDir = path.join(projectDir, name);
|
||||||
|
if (fs.existsSync(bmadDir)) {
|
||||||
|
return bmadDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update manifest to use absolute paths
|
||||||
|
*/
|
||||||
|
async function updateManifest(manifestPath, projectRoot) {
|
||||||
|
console.log(chalk.cyan(`\nUpdating manifest: ${manifestPath}`));
|
||||||
|
|
||||||
|
const content = await fs.readFile(manifestPath, 'utf8');
|
||||||
|
const manifest = yaml.parse(content);
|
||||||
|
|
||||||
|
if (!manifest.customModules || manifest.customModules.length === 0) {
|
||||||
|
console.log(chalk.dim(' No custom modules found'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = false;
|
||||||
|
|
||||||
|
for (const customModule of manifest.customModules) {
|
||||||
|
if (customModule.relativePath && !customModule.sourcePath) {
|
||||||
|
// Convert relative path to absolute
|
||||||
|
const absolutePath = path.resolve(projectRoot, customModule.relativePath);
|
||||||
|
customModule.sourcePath = absolutePath;
|
||||||
|
|
||||||
|
// Remove the old relativePath
|
||||||
|
delete customModule.relativePath;
|
||||||
|
|
||||||
|
console.log(chalk.green(` ✓ Updated ${customModule.id}: ${customModule.relativePath} → ${absolutePath}`));
|
||||||
|
updated = true;
|
||||||
|
} else if (customModule.sourcePath && !path.isAbsolute(customModule.sourcePath)) {
|
||||||
|
// Source path exists but is not absolute
|
||||||
|
const absolutePath = path.resolve(customModule.sourcePath);
|
||||||
|
customModule.sourcePath = absolutePath;
|
||||||
|
|
||||||
|
console.log(chalk.green(` ✓ Updated ${customModule.id}: ${customModule.sourcePath} → ${absolutePath}`));
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
// Write back the updated manifest
|
||||||
|
const yamlStr = yaml.dump(manifest, {
|
||||||
|
indent: 2,
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true,
|
||||||
|
sortKeys: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile(manifestPath, yamlStr);
|
||||||
|
console.log(chalk.green(' Manifest updated successfully'));
|
||||||
|
} else {
|
||||||
|
console.log(chalk.dim(' All paths already absolute'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main migration function
|
||||||
|
*/
|
||||||
|
async function migrate(directory) {
|
||||||
|
const projectRoot = path.resolve(directory || process.cwd());
|
||||||
|
const bmadDir = findBmadDir(projectRoot);
|
||||||
|
|
||||||
|
if (!bmadDir) {
|
||||||
|
console.error(chalk.red('✗ No BMAD installation found in directory'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.blue.bold('🔄 BMAD Custom Module Path Migration'));
|
||||||
|
console.log(chalk.dim(`Project: ${projectRoot}`));
|
||||||
|
console.log(chalk.dim(`BMAD Directory: ${bmadDir}`));
|
||||||
|
|
||||||
|
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
|
||||||
|
|
||||||
|
if (!fs.existsSync(manifestPath)) {
|
||||||
|
console.error(chalk.red('✗ No manifest.yaml found'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await updateManifest(manifestPath, projectRoot);
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
console.log(chalk.green.bold('\n✨ Migration completed successfully!'));
|
||||||
|
console.log(chalk.dim('Custom modules now use absolute source paths.'));
|
||||||
|
} else {
|
||||||
|
console.log(chalk.yellow('\n⚠ No migration needed - paths already absolute'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration if called directly
|
||||||
|
if (require.main === module) {
|
||||||
|
const directory = process.argv[2];
|
||||||
|
migrate(directory).catch((error) => {
|
||||||
|
console.error(chalk.red('\n✗ Migration failed:'), error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { migrate };
|
||||||
Loading…
Reference in New Issue