2025-09-28 23:17:07 -05:00
const path = require ( 'node:path' ) ;
const fs = require ( 'fs-extra' ) ;
const chalk = require ( 'chalk' ) ;
const { XmlHandler } = require ( '../../../lib/xml-handler' ) ;
const { getSourcePath } = require ( '../../../lib/project-root' ) ;
/ * *
* Base class for IDE - specific setup
* All IDE handlers should extend this class
* /
class BaseIdeSetup {
constructor ( name , displayName = null , preferred = false ) {
this . name = name ;
this . displayName = displayName || name ; // Human-readable name for UI
this . preferred = preferred ; // Whether this IDE should be shown in preferred list
this . configDir = null ; // Override in subclasses
this . rulesDir = null ; // Override in subclasses
2025-10-05 20:13:11 -07:00
this . configFile = null ; // Override in subclasses when detection is file-based
this . detectionPaths = [ ] ; // Additional paths that indicate the IDE is configured
2025-09-28 23:17:07 -05:00
this . xmlHandler = new XmlHandler ( ) ;
2025-11-08 15:19:19 -06:00
this . bmadFolderName = 'bmad' ; // Default, can be overridden
}
/ * *
* Set the bmad folder name for placeholder replacement
* @ param { string } bmadFolderName - The bmad folder name
* /
setBmadFolderName ( bmadFolderName ) {
this . bmadFolderName = bmadFolderName ;
2025-09-28 23:17:07 -05:00
}
Major Enhancements:
- Installation path is now fully configurable, allowing users to specify custom installation directories during setup
- Default installation location changed to .bmad (hidden directory) for cleaner project root organization
Web Bundle Improvements:
- All web bundles (single agent and team) now include party mode support for multi-agent collaboration!
- Advanced elicitation capabilities integrated into standalone agents
- All bundles enhanced with party mode agent manifests
- Added default-party.csv files to bmm, bmgd, and cis module teams
- The default party file is what will be used with single agent bundles. teams can customize for different party configurations before web bundling through a setting in the team yaml file
- New web bundle outputs for all agents (analyst, architect, dev, pm, sm, tea, tech-writer, ux-designer, game-*, creative-squad)
Phase 4 Workflow Updates (In Progress):
- Initiated shift to separate phase 4 implementation artifacts from documentation
- Phase 4 implementation artifacts (stories, code review, sprint plan, context files) will move to dedicated location outside docs folder
- Installer questions and configuration added for artifact path selection
- Updated workflow.yaml files for code-review, sprint-planning, story-context, epic-tech-context, and retrospective workflows to support this, but still might require some udpates
Additional Changes:
- New agent and action command header models for standardization
- Enhanced web-bundle-activation-steps fragment
- Updated web-bundler.js to support new structure
- VS Code settings updated for new .bmad directory
- Party mode instructions and workflow enhanced for better orchestration
IDE Installer Updates:
- Show version number of installer in cli
- improved Installer UX
- Gemini TOML Improved to have clear loading instructions with @ commands
- All tools agent launcher mds improved to use a central file template critical indication isntead of hardcoding in 2 different locations.
2025-11-09 17:39:05 -06:00
/ * *
* Get the agent command activation header from the central template
* @ returns { string } The activation header text ( without XML tags )
* /
async getAgentCommandHeader ( ) {
const headerPath = path . join ( getSourcePath ( ) , 'src' , 'utility' , 'models' , 'agent-command-header.md' ) ;
try {
const content = await fs . readFile ( headerPath , 'utf8' ) ;
// Strip the <critical> tags to get plain text
return content . replaceAll ( /<critical>|<\/critical>/g , '' ) . trim ( ) ;
} catch {
// Fallback if file doesn't exist
return "You must fully embody this agent's persona and follow all activation instructions, steps and rules exactly as specified. NEVER break character until given an exit command." ;
}
}
2025-09-28 23:17:07 -05:00
/ * *
* Main setup method - must be implemented by subclasses
* @ param { string } projectDir - Project directory
* @ param { string } bmadDir - BMAD installation directory
* @ param { Object } options - Setup options
* /
async setup ( projectDir , bmadDir , options = { } ) {
throw new Error ( ` setup() must be implemented by ${ this . name } handler ` ) ;
}
/ * *
* Cleanup IDE configuration
* @ param { string } projectDir - Project directory
* /
async cleanup ( projectDir ) {
// Default implementation - can be overridden
if ( this . configDir ) {
const configPath = path . join ( projectDir , this . configDir ) ;
if ( await fs . pathExists ( configPath ) ) {
const bmadRulesPath = path . join ( configPath , 'bmad' ) ;
if ( await fs . pathExists ( bmadRulesPath ) ) {
await fs . remove ( bmadRulesPath ) ;
console . log ( chalk . dim ( ` Removed ${ this . name } BMAD configuration ` ) ) ;
}
}
}
}
feat: Complete BMAD agent creation system with install tooling, references, and field guidance
## Overview
This commit represents a complete overhaul of the BMAD agent creation system, establishing clear standards for agent development, installation workflows, and persona design. The changes span documentation, tooling, reference implementations, and field-specific guidance.
## Key Components
### 1. Agent Installation Infrastructure
**New CLI Command: `agent-install`**
- Interactive agent installation with persona customization
- Supports Simple (single YAML), Expert (sidecar files), and Module agents
- Template variable processing with Handlebars-style syntax
- Automatic compilation from YAML to XML (.md) format
- Manifest tracking and IDE integration (Claude Code, Cursor, Windsurf, etc.)
- Source preservation in `_cfg/custom/agents/` for reinstallation
**Files Created:**
- `tools/cli/commands/agent-install.js` - Main CLI command
- `tools/cli/lib/agent/compiler.js` - YAML to XML compilation engine
- `tools/cli/lib/agent/installer.js` - Installation orchestration
- `tools/cli/lib/agent/template-engine.js` - Handlebars template processing
**Compiler Features:**
- Auto-injects frontmatter, activation, handlers, help/exit menu items
- Smart handler inclusion (only includes action/workflow/exec/tmpl handlers actually used)
- Proper XML escaping and formatting
- Persona name customization (e.g., "Fred the Commit Poet")
### 2. Documentation Overhaul
**Deleted Bloated/Outdated Docs (2,651 lines removed):**
- Old verbose architecture docs
- Redundant pattern files
- Outdated workflow guides
**Created Focused, Type-Specific Docs:**
- `src/modules/bmb/docs/understanding-agent-types.md` - Architecture vs capability distinction
- `src/modules/bmb/docs/simple-agent-architecture.md` - Self-contained agents
- `src/modules/bmb/docs/expert-agent-architecture.md` - Agents with sidecar files
- `src/modules/bmb/docs/module-agent-architecture.md` - Workflow-integrated agents
- `src/modules/bmb/docs/agent-compilation.md` - YAML → XML process
- `src/modules/bmb/docs/agent-menu-patterns.md` - Menu design patterns
- `src/modules/bmb/docs/index.md` - Documentation hub
**Net Result:** ~1,930 line reduction while adding MORE value through focused content
### 3. Create-Agent Workflow Enhancements
**Critical Persona Field Guidance Added to Step 4:**
Explains how the LLM interprets each persona field when the agent activates:
- **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, quirks, 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 restate role/identity/principles. The `communication-presets.csv` provides 60 pure communication styles with NO role/identity/principles mixed in.
**Files Updated:**
- `src/modules/bmb/workflows/create-agent/instructions.md` - Added persona field interpretation guide
- `src/modules/bmb/workflows/create-agent/brainstorm-context.md` - Refined to 137 lines
- `src/modules/bmb/workflows/create-agent/communication-presets.csv` - 60 styles across 13 categories
### 4. Reference Agent Cleanup
**Removed install_config Personality Bloat:**
Understanding: Future installer will handle personality customization, so stripped all personality toggles from reference agents.
**commit-poet.agent.yaml** (Simple Agent):
- BEFORE: 36 personality combinations (3 enthusiasm × 3 depths × 4 styles) = decision fatigue
- AFTER: Single concise persona with pure communication style
- Changed from verbose conditionals to: "Poetic drama and flair with every turn of a phrase. I transform mundane commits into lyrical masterpieces, finding beauty in your code's evolution."
- Reduction: 248 lines → 153 lines (38% reduction)
**journal-keeper.agent.yaml** (Expert Agent):
- Stripped install_config, simplified communication_style
- Shows proper Expert agent structure with sidecar files
**security-engineer.agent.yaml & trend-analyst.agent.yaml** (Module Agents):
- Added header comments explaining WHY Module Agent (design intent, not just location)
- Clarified: Module agents are designed FOR ecosystem integration, not capability-limited
**Files Updated:**
- `src/modules/bmb/reference/agents/simple-examples/commit-poet.agent.yaml`
- `src/modules/bmb/reference/agents/expert-examples/journal-keeper/journal-keeper.agent.yaml`
- `src/modules/bmb/reference/agents/module-examples/security-engineer.agent.yaml`
- `src/modules/bmb/reference/agents/module-examples/trend-analyst.agent.yaml`
### 5. BMM Agent Voice Enhancement
**Gave all 9 BMM agents distinct, memorable communication voices:**
**Mary (analyst)** - The favorite! Changed from generic "systematic and probing" to:
"Treats analysis like a treasure hunt - excited by every clue, thrilled when patterns emerge. Asks questions that spark 'aha!' moments while structuring insights with precision."
**Other Notable Voices:**
- **John (pm):** "Asks 'WHY?' relentlessly like a detective on a case. Direct and data-sharp, cuts through fluff to what actually matters."
- **Winston (architect):** "Speaks in calm, pragmatic tones, balancing 'what could be' with 'what should be.' Champions boring technology that actually works."
- **Amelia (dev):** "Ultra-succinct. Speaks in file paths and AC IDs - every statement citable. No fluff, all precision."
- **Bob (sm):** "Crisp and checklist-driven. Every word has a purpose, every requirement crystal clear. Zero tolerance for ambiguity."
- **Sally (ux-designer):** "Paints pictures with words, telling user stories that make you FEEL the problem. Empathetic advocate with creative storytelling flair."
**Pattern Applied:** Moved behaviors from communication_style to principles, keeping communication_style as PURE verbal patterns.
**Files Updated:**
- `src/modules/bmm/agents/analyst.agent.yaml`
- `src/modules/bmm/agents/pm.agent.yaml`
- `src/modules/bmm/agents/architect.agent.yaml`
- `src/modules/bmm/agents/dev.agent.yaml`
- `src/modules/bmm/agents/sm.agent.yaml`
- `src/modules/bmm/agents/tea.agent.yaml`
- `src/modules/bmm/agents/tech-writer.agent.yaml`
- `src/modules/bmm/agents/ux-designer.agent.yaml`
- `src/modules/bmm/agents/frame-expert.agent.yaml`
### 6. Linting Fixes
**ESLint Compliance:**
- Replaced all `'utf-8'` with `'utf8'` (unicorn/text-encoding-identifier-case)
- Changed `variables.hasOwnProperty(varName)` to `Object.hasOwn(variables, varName)` (unicorn/prefer-object-has-own)
- Replaced `JSON.parse(JSON.stringify(...))` with `structuredClone(...)` (unicorn/prefer-structured-clone)
- Fixed empty YAML mapping values in sample files
**Files Fixed:**
- 7 JavaScript files across agent tooling (compiler, installer, commands, IDE integration)
- 1 YAML sample file
## Architecture Decisions
### Agent Types Are About Architecture, Not Capability
- **Simple:** Self-contained in single YAML (NOT limited in capability)
- **Expert:** Includes sidecar files (templates, docs, etc.)
- **Module:** Designed for BMAD ecosystem integration (workflows, cross-agent coordination)
### Persona Field Separation Critical for LLM Interpretation
The LLM needs distinct fields to understand its role:
- Mixing role/identity/principles into communication_style confuses the persona
- Pure communication styles (from communication-presets.csv) have ZERO role/identity/principles content
- Example DON'T: "Experienced analyst who uses systematic approaches..." (mixing identity + style)
- Example DO: "Systematic and probing. Structures findings hierarchically." (pure style)
### Install-Time vs Runtime Configuration
- Template variables ({{var}}) resolve at compile-time
- Runtime variables ({user_name}, {bmad_folder}) resolve when agent activates
- Future installer will handle personality customization, so agents should ship with single default persona
## Testing
- All linting passes (ESLint with max-warnings=0)
- Agent compilation tested with commit-poet, journal-keeper examples
- Install workflow validated with Simple and Expert agent types
- Manifest tracking and IDE integration verified
## Impact
This establishes BMAD as having a complete, production-ready agent creation and installation system with:
- Clear documentation for all agent types
- Automated compilation and installation
- Strong persona design guidance
- Reference implementations showing best practices
- Distinct, memorable agent voices throughout BMM module
Co-Authored-By: BMad Builder <builder@bmad.dev>
Co-Authored-By: Mary the Analyst <analyst@bmad.dev>
Co-Authored-By: Paige the Tech Writer <tech-writer@bmad.dev>
2025-11-17 22:25:15 -06:00
/ * *
* Install a custom agent launcher - subclasses should override
* @ param { string } projectDir - Project directory
* @ param { string } agentName - Agent name ( e . g . , "fred-commit-poet" )
* @ param { string } agentPath - Path to compiled agent ( relative to project root )
* @ param { Object } metadata - Agent metadata
* @ returns { Object | null } Info about created command , or null if not supported
* /
async installCustomAgentLauncher ( projectDir , agentName , agentPath , metadata ) {
// Default implementation - subclasses can override
return null ;
}
2025-10-05 20:13:11 -07:00
/ * *
* Detect whether this IDE already has configuration in the project
* Subclasses can override for custom logic
* @ param { string } projectDir - Project directory
* @ returns { boolean }
* /
async detect ( projectDir ) {
const pathsToCheck = [ ] ;
if ( this . configDir ) {
pathsToCheck . push ( path . join ( projectDir , this . configDir ) ) ;
}
if ( this . configFile ) {
pathsToCheck . push ( path . join ( projectDir , this . configFile ) ) ;
}
if ( Array . isArray ( this . detectionPaths ) ) {
for ( const candidate of this . detectionPaths ) {
if ( ! candidate ) continue ;
const resolved = path . isAbsolute ( candidate ) ? candidate : path . join ( projectDir , candidate ) ;
pathsToCheck . push ( resolved ) ;
}
}
for ( const candidate of pathsToCheck ) {
if ( await fs . pathExists ( candidate ) ) {
return true ;
}
}
return false ;
}
2025-09-28 23:17:07 -05:00
/ * *
* Get list of agents from BMAD installation
* @ param { string } bmadDir - BMAD installation directory
* @ returns { Array } List of agent files
* /
async getAgents ( bmadDir ) {
const agents = [ ] ;
// Get core agents
const coreAgentsPath = path . join ( bmadDir , 'core' , 'agents' ) ;
if ( await fs . pathExists ( coreAgentsPath ) ) {
const coreAgents = await this . scanDirectory ( coreAgentsPath , '.md' ) ;
agents . push (
... coreAgents . map ( ( a ) => ( {
... a ,
module : 'core' ,
} ) ) ,
) ;
}
// Get module agents
const entries = await fs . readdir ( bmadDir , { withFileTypes : true } ) ;
for ( const entry of entries ) {
2025-10-09 23:07:12 -05:00
if ( entry . isDirectory ( ) && entry . name !== 'core' && entry . name !== '_cfg' && entry . name !== 'agents' ) {
2025-09-28 23:17:07 -05:00
const moduleAgentsPath = path . join ( bmadDir , entry . name , 'agents' ) ;
if ( await fs . pathExists ( moduleAgentsPath ) ) {
const moduleAgents = await this . scanDirectory ( moduleAgentsPath , '.md' ) ;
agents . push (
... moduleAgents . map ( ( a ) => ( {
... a ,
module : entry . name ,
} ) ) ,
) ;
}
}
}
2025-10-09 23:07:12 -05:00
// Get standalone agents from bmad/agents/ directory
const standaloneAgentsDir = path . join ( bmadDir , 'agents' ) ;
if ( await fs . pathExists ( standaloneAgentsDir ) ) {
const agentDirs = await fs . readdir ( standaloneAgentsDir , { withFileTypes : true } ) ;
for ( const agentDir of agentDirs ) {
if ( ! agentDir . isDirectory ( ) ) continue ;
const agentDirPath = path . join ( standaloneAgentsDir , agentDir . name ) ;
const agentFiles = await fs . readdir ( agentDirPath ) ;
for ( const file of agentFiles ) {
if ( ! file . endsWith ( '.md' ) ) continue ;
if ( file . includes ( '.customize.' ) ) continue ;
const filePath = path . join ( agentDirPath , file ) ;
const content = await fs . readFile ( filePath , 'utf8' ) ;
if ( content . includes ( 'localskip="true"' ) ) continue ;
agents . push ( {
name : file . replace ( '.md' , '' ) ,
path : filePath ,
relativePath : path . relative ( standaloneAgentsDir , filePath ) ,
filename : file ,
module : 'standalone' , // Mark as standalone agent
} ) ;
}
}
}
2025-09-28 23:17:07 -05:00
return agents ;
}
/ * *
* Get list of tasks from BMAD installation
* @ param { string } bmadDir - BMAD installation directory
2025-10-26 19:38:38 -05:00
* @ param { boolean } standaloneOnly - If true , only return standalone tasks
2025-09-28 23:17:07 -05:00
* @ returns { Array } List of task files
* /
2025-10-26 19:38:38 -05:00
async getTasks ( bmadDir , standaloneOnly = false ) {
2025-09-28 23:17:07 -05:00
const tasks = [ ] ;
2025-10-26 19:38:38 -05:00
// Get core tasks (scan for both .md and .xml)
2025-09-28 23:17:07 -05:00
const coreTasksPath = path . join ( bmadDir , 'core' , 'tasks' ) ;
if ( await fs . pathExists ( coreTasksPath ) ) {
2025-10-26 19:38:38 -05:00
const coreTasks = await this . scanDirectoryWithStandalone ( coreTasksPath , [ '.md' , '.xml' ] ) ;
2025-09-28 23:17:07 -05:00
tasks . push (
... coreTasks . map ( ( t ) => ( {
... t ,
module : 'core' ,
} ) ) ,
) ;
}
// Get module tasks
const entries = await fs . readdir ( bmadDir , { withFileTypes : true } ) ;
for ( const entry of entries ) {
2025-10-09 23:07:12 -05:00
if ( entry . isDirectory ( ) && entry . name !== 'core' && entry . name !== '_cfg' && entry . name !== 'agents' ) {
2025-09-28 23:17:07 -05:00
const moduleTasksPath = path . join ( bmadDir , entry . name , 'tasks' ) ;
if ( await fs . pathExists ( moduleTasksPath ) ) {
2025-10-26 19:38:38 -05:00
const moduleTasks = await this . scanDirectoryWithStandalone ( moduleTasksPath , [ '.md' , '.xml' ] ) ;
2025-09-28 23:17:07 -05:00
tasks . push (
... moduleTasks . map ( ( t ) => ( {
... t ,
module : entry . name ,
} ) ) ,
) ;
}
}
}
2025-10-26 19:38:38 -05:00
// Filter by standalone if requested
if ( standaloneOnly ) {
return tasks . filter ( ( t ) => t . standalone === true ) ;
}
2025-09-28 23:17:07 -05:00
return tasks ;
}
/ * *
2025-10-26 19:38:38 -05:00
* Get list of tools from BMAD installation
* @ param { string } bmadDir - BMAD installation directory
* @ param { boolean } standaloneOnly - If true , only return standalone tools
* @ returns { Array } List of tool files
* /
async getTools ( bmadDir , standaloneOnly = false ) {
const tools = [ ] ;
// Get core tools (scan for both .md and .xml)
const coreToolsPath = path . join ( bmadDir , 'core' , 'tools' ) ;
if ( await fs . pathExists ( coreToolsPath ) ) {
const coreTools = await this . scanDirectoryWithStandalone ( coreToolsPath , [ '.md' , '.xml' ] ) ;
tools . push (
... coreTools . map ( ( t ) => ( {
... t ,
module : 'core' ,
} ) ) ,
) ;
}
// Get module tools
const entries = await fs . readdir ( bmadDir , { withFileTypes : true } ) ;
for ( const entry of entries ) {
if ( entry . isDirectory ( ) && entry . name !== 'core' && entry . name !== '_cfg' && entry . name !== 'agents' ) {
const moduleToolsPath = path . join ( bmadDir , entry . name , 'tools' ) ;
if ( await fs . pathExists ( moduleToolsPath ) ) {
const moduleTools = await this . scanDirectoryWithStandalone ( moduleToolsPath , [ '.md' , '.xml' ] ) ;
tools . push (
... moduleTools . map ( ( t ) => ( {
... t ,
module : entry . name ,
} ) ) ,
) ;
}
}
}
// Filter by standalone if requested
if ( standaloneOnly ) {
return tools . filter ( ( t ) => t . standalone === true ) ;
}
return tools ;
}
/ * *
* Get list of workflows from BMAD installation
* @ param { string } bmadDir - BMAD installation directory
* @ param { boolean } standaloneOnly - If true , only return standalone workflows
* @ returns { Array } List of workflow files
* /
async getWorkflows ( bmadDir , standaloneOnly = false ) {
const workflows = [ ] ;
// Get core workflows
const coreWorkflowsPath = path . join ( bmadDir , 'core' , 'workflows' ) ;
if ( await fs . pathExists ( coreWorkflowsPath ) ) {
const coreWorkflows = await this . findWorkflowYamlFiles ( coreWorkflowsPath ) ;
workflows . push (
... coreWorkflows . map ( ( w ) => ( {
... w ,
module : 'core' ,
} ) ) ,
) ;
}
// Get module workflows
const entries = await fs . readdir ( bmadDir , { withFileTypes : true } ) ;
for ( const entry of entries ) {
if ( entry . isDirectory ( ) && entry . name !== 'core' && entry . name !== '_cfg' && entry . name !== 'agents' ) {
const moduleWorkflowsPath = path . join ( bmadDir , entry . name , 'workflows' ) ;
if ( await fs . pathExists ( moduleWorkflowsPath ) ) {
const moduleWorkflows = await this . findWorkflowYamlFiles ( moduleWorkflowsPath ) ;
workflows . push (
... moduleWorkflows . map ( ( w ) => ( {
... w ,
module : entry . name ,
} ) ) ,
) ;
}
}
}
// Filter by standalone if requested
if ( standaloneOnly ) {
return workflows . filter ( ( w ) => w . standalone === true ) ;
}
return workflows ;
}
/ * *
* Recursively find workflow . yaml files
* @ param { string } dir - Directory to search
* @ returns { Array } List of workflow file info objects
* /
async findWorkflowYamlFiles ( dir ) {
const workflows = [ ] ;
if ( ! ( await fs . pathExists ( dir ) ) ) {
return workflows ;
}
const entries = await fs . readdir ( dir , { withFileTypes : true } ) ;
for ( const entry of entries ) {
const fullPath = path . join ( dir , entry . name ) ;
if ( entry . isDirectory ( ) ) {
// Recursively search subdirectories
const subWorkflows = await this . findWorkflowYamlFiles ( fullPath ) ;
workflows . push ( ... subWorkflows ) ;
} else if ( entry . isFile ( ) && entry . name === 'workflow.yaml' ) {
// Read workflow.yaml to get name and standalone property
try {
const yaml = require ( 'js-yaml' ) ;
const content = await fs . readFile ( fullPath , 'utf8' ) ;
const workflowData = yaml . load ( content ) ;
if ( workflowData && workflowData . name ) {
workflows . push ( {
name : workflowData . name ,
path : fullPath ,
relativePath : path . relative ( dir , fullPath ) ,
filename : entry . name ,
description : workflowData . description || '' ,
standalone : workflowData . standalone === true , // Check standalone property
} ) ;
}
} catch {
// Skip invalid workflow files
}
}
}
return workflows ;
}
/ * *
* Scan a directory for files with specific extension ( s )
2025-09-28 23:17:07 -05:00
* @ param { string } dir - Directory to scan
2025-10-26 19:38:38 -05:00
* @ param { string | Array < string > } ext - File extension ( s ) to match ( e . g . , '.md' or [ '.md' , '.xml' ] )
2025-09-28 23:17:07 -05:00
* @ returns { Array } List of file info objects
* /
async scanDirectory ( dir , ext ) {
const files = [ ] ;
if ( ! ( await fs . pathExists ( dir ) ) ) {
return files ;
}
2025-10-26 19:38:38 -05:00
// Normalize ext to array
const extensions = Array . isArray ( ext ) ? ext : [ ext ] ;
2025-09-28 23:17:07 -05:00
const entries = await fs . readdir ( dir , { withFileTypes : true } ) ;
for ( const entry of entries ) {
const fullPath = path . join ( dir , entry . name ) ;
if ( entry . isDirectory ( ) ) {
// Recursively scan subdirectories
const subFiles = await this . scanDirectory ( fullPath , ext ) ;
files . push ( ... subFiles ) ;
2025-10-26 19:38:38 -05:00
} else if ( entry . isFile ( ) ) {
// Check if file matches any of the extensions
const matchedExt = extensions . find ( ( e ) => entry . name . endsWith ( e ) ) ;
if ( matchedExt ) {
files . push ( {
name : path . basename ( entry . name , matchedExt ) ,
path : fullPath ,
relativePath : path . relative ( dir , fullPath ) ,
filename : entry . name ,
} ) ;
}
}
}
return files ;
}
/ * *
* Scan a directory for files with specific extension ( s ) and check standalone attribute
* @ param { string } dir - Directory to scan
* @ param { string | Array < string > } ext - File extension ( s ) to match ( e . g . , '.md' or [ '.md' , '.xml' ] )
* @ returns { Array } List of file info objects with standalone property
* /
async scanDirectoryWithStandalone ( dir , ext ) {
const files = [ ] ;
if ( ! ( await fs . pathExists ( dir ) ) ) {
return files ;
}
// Normalize ext to array
const extensions = Array . isArray ( ext ) ? ext : [ ext ] ;
const entries = await fs . readdir ( dir , { withFileTypes : true } ) ;
for ( const entry of entries ) {
const fullPath = path . join ( dir , entry . name ) ;
if ( entry . isDirectory ( ) ) {
// Recursively scan subdirectories
const subFiles = await this . scanDirectoryWithStandalone ( fullPath , ext ) ;
files . push ( ... subFiles ) ;
} else if ( entry . isFile ( ) ) {
// Check if file matches any of the extensions
const matchedExt = extensions . find ( ( e ) => entry . name . endsWith ( e ) ) ;
if ( matchedExt ) {
// Read file content to check for standalone attribute
let standalone = false ;
try {
const content = await fs . readFile ( fullPath , 'utf8' ) ;
// Check for standalone="true" in XML files
if ( entry . name . endsWith ( '.xml' ) ) {
// Look for standalone="true" in the opening tag (task or tool)
const standaloneMatch = content . match ( /<(?:task|tool)[^>]+standalone="true"/ ) ;
standalone = ! ! standaloneMatch ;
} else if ( entry . name . endsWith ( '.md' ) ) {
// Check for standalone: true in YAML frontmatter
const frontmatterMatch = content . match ( /^---\s*\n([\s\S]*?)\n---/ ) ;
if ( frontmatterMatch ) {
const yaml = require ( 'js-yaml' ) ;
try {
const frontmatter = yaml . load ( frontmatterMatch [ 1 ] ) ;
standalone = frontmatter . standalone === true ;
} catch {
// Ignore YAML parse errors
}
}
}
} catch {
// If we can't read the file, assume not standalone
standalone = false ;
}
files . push ( {
name : path . basename ( entry . name , matchedExt ) ,
path : fullPath ,
relativePath : path . relative ( dir , fullPath ) ,
filename : entry . name ,
standalone : standalone ,
} ) ;
}
2025-09-28 23:17:07 -05:00
}
}
return files ;
}
/ * *
* Create IDE command / rule file from agent or task
* @ param { string } content - File content
* @ param { Object } metadata - File metadata
* @ param { string } projectDir - The actual project directory path
* @ returns { string } Processed content
* /
processContent ( content , metadata = { } , projectDir = null ) {
// Replace placeholders
let processed = content ;
// Inject activation block for agent files FIRST (before replacements)
if ( metadata . name && content . includes ( '<agent' ) ) {
processed = this . xmlHandler . injectActivationSimple ( processed , metadata ) ;
}
2025-10-15 21:17:09 -05:00
// Only replace {project-root} if a specific projectDir is provided
// Otherwise leave the placeholder intact
2025-10-02 21:45:59 -05:00
// Note: Don't add trailing slash - paths in source include leading slash
2025-10-15 21:17:09 -05:00
if ( projectDir ) {
processed = processed . replaceAll ( '{project-root}' , projectDir ) ;
}
2025-09-28 23:17:07 -05:00
processed = processed . replaceAll ( '{module}' , metadata . module || 'core' ) ;
processed = processed . replaceAll ( '{agent}' , metadata . name || '' ) ;
processed = processed . replaceAll ( '{task}' , metadata . name || '' ) ;
return processed ;
}
/ * *
* Ensure directory exists
* @ param { string } dirPath - Directory path
* /
async ensureDir ( dirPath ) {
await fs . ensureDir ( dirPath ) ;
}
/ * *
2025-11-08 15:19:19 -06:00
* Write file with content ( replaces { bmad _folder } placeholder )
2025-09-28 23:17:07 -05:00
* @ param { string } filePath - File path
* @ param { string } content - File content
* /
async writeFile ( filePath , content ) {
2025-11-08 15:19:19 -06:00
// Replace {bmad_folder} placeholder if present
if ( typeof content === 'string' && content . includes ( '{bmad_folder}' ) ) {
content = content . replaceAll ( '{bmad_folder}' , this . bmadFolderName ) ;
}
2025-12-02 22:36:44 -06:00
// Replace escape sequence {*bmad_folder*} with literal {bmad_folder}
if ( typeof content === 'string' && content . includes ( '{*bmad_folder*}' ) ) {
content = content . replaceAll ( '{*bmad_folder*}' , '{bmad_folder}' ) ;
}
2025-09-28 23:17:07 -05:00
await this . ensureDir ( path . dirname ( filePath ) ) ;
await fs . writeFile ( filePath , content , 'utf8' ) ;
}
/ * *
2025-11-08 15:19:19 -06:00
* Copy file from source to destination ( replaces { bmad _folder } placeholder in text files )
2025-09-28 23:17:07 -05:00
* @ param { string } source - Source file path
* @ param { string } dest - Destination file path
* /
async copyFile ( source , dest ) {
2025-11-08 15:19:19 -06:00
// 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 ext = path . extname ( source ) . toLowerCase ( ) ;
2025-09-28 23:17:07 -05:00
await this . ensureDir ( path . dirname ( dest ) ) ;
2025-11-08 15:19:19 -06:00
// Check if this is a text file that might contain placeholders
if ( textExtensions . includes ( ext ) ) {
try {
// Read the file content
let content = await fs . readFile ( source , 'utf8' ) ;
// Replace {bmad_folder} placeholder with actual folder name
if ( content . includes ( '{bmad_folder}' ) ) {
content = content . replaceAll ( '{bmad_folder}' , this . bmadFolderName ) ;
}
2025-12-02 22:36:44 -06:00
// Replace escape sequence {*bmad_folder*} with literal {bmad_folder}
if ( content . includes ( '{*bmad_folder*}' ) ) {
content = content . replaceAll ( '{*bmad_folder*}' , '{bmad_folder}' ) ;
}
2025-11-08 15:19:19 -06:00
// Write to dest with replaced content
await fs . writeFile ( dest , content , 'utf8' ) ;
} catch {
// If reading as text fails, fall back to regular copy
await fs . copy ( source , dest , { overwrite : true } ) ;
}
} else {
// Binary file or other file type - just copy directly
await fs . copy ( source , dest , { overwrite : true } ) ;
}
2025-09-28 23:17:07 -05:00
}
/ * *
* Check if path exists
* @ param { string } pathToCheck - Path to check
* @ returns { boolean } True if path exists
* /
async exists ( pathToCheck ) {
return await fs . pathExists ( pathToCheck ) ;
}
/ * *
* Alias for exists method
* @ param { string } pathToCheck - Path to check
* @ returns { boolean } True if path exists
* /
async pathExists ( pathToCheck ) {
return await fs . pathExists ( pathToCheck ) ;
}
/ * *
* Read file content
* @ param { string } filePath - File path
* @ returns { string } File content
* /
async readFile ( filePath ) {
return await fs . readFile ( filePath , 'utf8' ) ;
}
/ * *
* Format name as title
* @ param { string } name - Name to format
* @ returns { string } Formatted title
* /
formatTitle ( name ) {
return name
. split ( '-' )
. map ( ( word ) => word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) )
. join ( ' ' ) ;
}
2025-11-18 18:09:06 -08:00
/ * *
* Flatten a relative path to a single filename for flat slash command naming
* Example : 'module/agents/name.md' - > 'bmad-module-agents-name.md'
* Used by IDEs that ignore directory structure for slash commands ( e . g . , Antigravity , Codex )
* @ param { string } relativePath - Relative path to flatten
* @ returns { string } Flattened filename with 'bmad-' prefix
* /
flattenFilename ( relativePath ) {
const sanitized = relativePath . replaceAll ( /[/\\]/g , '-' ) ;
return ` bmad- ${ sanitized } ` ;
}
2025-09-28 23:17:07 -05:00
/ * *
* Create agent configuration file
* @ param { string } bmadDir - BMAD installation directory
* @ param { Object } agent - Agent information
* /
async createAgentConfig ( bmadDir , agent ) {
const agentConfigDir = path . join ( bmadDir , '_cfg' , 'agents' ) ;
await this . ensureDir ( agentConfigDir ) ;
// Load agent config template
const templatePath = getSourcePath ( 'utility' , 'models' , 'agent-config-template.md' ) ;
const templateContent = await this . readFile ( templatePath ) ;
const configContent = ` # Agent Config: ${ agent . name }
$ { templateContent } ` ;
const configPath = path . join ( agentConfigDir , ` ${ agent . module } - ${ agent . name } .md ` ) ;
await this . writeFile ( configPath , configContent ) ;
}
}
module . exports = { BaseIdeSetup } ;