2025-08-16 19:08:39 -05:00
const fs = require ( 'node:fs' ) . promises ;
const path = require ( 'node:path' ) ;
const DependencyResolver = require ( '../lib/dependency-resolver' ) ;
const yamlUtilities = require ( '../lib/yaml-utils' ) ;
2025-06-10 21:41:58 -05:00
class WebBuilder {
constructor ( options = { } ) {
this . rootDir = options . rootDir || process . cwd ( ) ;
2025-08-16 19:08:39 -05:00
this . outputDirs = options . outputDirs || [ path . join ( this . rootDir , 'dist' ) ] ;
2025-06-10 21:41:58 -05:00
this . resolver = new DependencyResolver ( this . rootDir ) ;
2025-06-19 12:55:16 -05:00
this . templatePath = path . join (
this . rootDir ,
2025-08-16 19:08:39 -05:00
'tools' ,
'md-assets' ,
'web-agent-startup-instructions.md' ,
2025-06-19 12:55:16 -05:00
) ;
2025-06-10 21:41:58 -05:00
}
2025-06-19 01:07:21 +02:00
parseYaml ( content ) {
2025-08-16 19:08:39 -05:00
const yaml = require ( 'js-yaml' ) ;
2025-06-19 01:07:21 +02:00
return yaml . load ( content ) ;
}
2025-07-06 10:32:39 -05:00
convertToWebPath ( filePath , bundleRoot = 'bmad-core' ) {
// Convert absolute paths to web bundle paths with dot prefix
// All resources get installed under the bundle root, so use that path
const relativePath = path . relative ( this . rootDir , filePath ) ;
const pathParts = relativePath . split ( path . sep ) ;
2025-08-16 19:08:39 -05:00
2025-07-06 10:32:39 -05:00
let resourcePath ;
if ( pathParts [ 0 ] === 'expansion-packs' ) {
// For expansion packs, remove 'expansion-packs/packname' and use the rest
resourcePath = pathParts . slice ( 2 ) . join ( '/' ) ;
} else {
// For bmad-core, common, etc., remove the first part
resourcePath = pathParts . slice ( 1 ) . join ( '/' ) ;
}
2025-08-16 19:08:39 -05:00
2025-07-06 10:32:39 -05:00
return ` . ${ bundleRoot } / ${ resourcePath } ` ;
}
generateWebInstructions ( bundleType , packName = null ) {
// Generate dynamic web instructions based on bundle type
2025-08-18 09:46:16 -05:00
const rootExample = packName ? ` . ${ packName } ` : 'bmad-core' ;
2025-08-16 19:08:39 -05:00
const examplePath = packName
? ` . ${ packName } /folder/filename.md `
2025-08-18 09:46:16 -05:00
: 'bmad-core/folder/filename.md' ;
2025-08-16 19:08:39 -05:00
const personasExample = packName
? ` . ${ packName } /personas/analyst.md `
2025-08-18 09:46:16 -05:00
: 'bmad-core/personas/analyst.md' ;
2025-08-16 19:08:39 -05:00
const tasksExample = packName
? ` . ${ packName } /tasks/create-story.md `
2025-08-18 09:46:16 -05:00
: 'bmad-core/tasks/create-story.md' ;
2025-08-16 19:08:39 -05:00
const utilitiesExample = packName
? ` . ${ packName } /utils/template-format.md `
2025-08-18 09:46:16 -05:00
: 'bmad-core/utils/template-format.md' ;
2025-08-16 19:08:39 -05:00
const tasksReference = packName
? ` . ${ packName } /tasks/create-story.md `
2025-08-18 09:46:16 -05:00
: 'bmad-core/tasks/create-story.md' ;
2025-07-06 10:32:39 -05:00
return ` # Web Agent Bundle Instructions
You are now operating as a specialized AI agent from the BMad - Method framework . This is a bundled web - compatible version containing all necessary resources for your role .
# # Important Instructions
1. * * Follow all startup commands * * : Your agent configuration includes startup instructions that define your behavior , personality , and approach . These MUST be followed exactly .
2. * * Resource Navigation * * : This bundle contains all resources you need . Resources are marked with tags like :
- \ ` ==================== START: ${ examplePath } ==================== \`
- \ ` ==================== END: ${ examplePath } ==================== \`
When you need to reference a resource mentioned in your instructions :
- Look for the corresponding START / END tags
- The format is always the full path with dot prefix ( e . g . , \ ` ${ personasExample } \` , \` ${ tasksExample } \` )
- If a section is specified ( e . g . , \ ` {root}/tasks/create-story.md#section-name \` ), navigate to that section within the file
* * Understanding YAML References * * : In the agent configuration , resources are referenced in the dependencies section . For example :
\ ` \` \` yaml
dependencies :
utils :
- template - format
tasks :
- create - story
\ ` \` \`
These references map directly to bundle sections :
2025-08-16 19:08:39 -05:00
- \ ` utils: template-format \` → Look for \` ==================== START: ${ utilitiesExample } ==================== \`
- \ ` tasks: create-story \` → Look for \` ==================== START: ${ tasksReference } ==================== \`
2025-07-06 10:32:39 -05:00
3. * * Execution Context * * : You are operating in a web environment . All your capabilities and knowledge are contained within this bundle . Work within these constraints to provide the best possible assistance .
4. * * Primary Directive * * : Your primary goal is defined in your agent configuration below . Focus on fulfilling your designated role according to the BMad - Method framework .
-- -
` ;
}
2025-06-10 21:41:58 -05:00
async cleanOutputDirs ( ) {
for ( const dir of this . outputDirs ) {
try {
await fs . rm ( dir , { recursive : true , force : true } ) ;
console . log ( ` Cleaned: ${ path . relative ( this . rootDir , dir ) } ` ) ;
} catch ( error ) {
2025-06-15 07:59:25 -07:00
console . debug ( ` Failed to clean directory ${ dir } : ` , error . message ) ;
2025-06-10 21:41:58 -05:00
// Directory might not exist, that's fine
}
}
}
async buildAgents ( ) {
const agents = await this . resolver . listAgents ( ) ;
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
for ( const agentId of agents ) {
console . log ( ` Building agent: ${ agentId } ` ) ;
const bundle = await this . buildAgentBundle ( agentId ) ;
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
// Write to all output directories
for ( const outputDir of this . outputDirs ) {
2025-08-16 19:08:39 -05:00
const outputPath = path . join ( outputDir , 'agents' ) ;
2025-06-10 21:41:58 -05:00
await fs . mkdir ( outputPath , { recursive : true } ) ;
2025-06-11 08:13:36 -05:00
const outputFile = path . join ( outputPath , ` ${ agentId } .txt ` ) ;
2025-08-16 19:08:39 -05:00
await fs . writeFile ( outputFile , bundle , 'utf8' ) ;
2025-06-10 21:41:58 -05:00
}
}
console . log ( ` Built ${ agents . length } agent bundles in ${ this . outputDirs . length } locations ` ) ;
}
async buildTeams ( ) {
const teams = await this . resolver . listTeams ( ) ;
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
for ( const teamId of teams ) {
console . log ( ` Building team: ${ teamId } ` ) ;
const bundle = await this . buildTeamBundle ( teamId ) ;
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
// Write to all output directories
for ( const outputDir of this . outputDirs ) {
2025-08-16 19:08:39 -05:00
const outputPath = path . join ( outputDir , 'teams' ) ;
2025-06-10 21:41:58 -05:00
await fs . mkdir ( outputPath , { recursive : true } ) ;
2025-06-11 08:13:36 -05:00
const outputFile = path . join ( outputPath , ` ${ teamId } .txt ` ) ;
2025-08-16 19:08:39 -05:00
await fs . writeFile ( outputFile , bundle , 'utf8' ) ;
2025-06-10 21:41:58 -05:00
}
}
console . log ( ` Built ${ teams . length } team bundles in ${ this . outputDirs . length } locations ` ) ;
}
async buildAgentBundle ( agentId ) {
const dependencies = await this . resolver . resolveAgentDependencies ( agentId ) ;
2025-07-06 10:32:39 -05:00
const template = this . generateWebInstructions ( 'agent' ) ;
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
const sections = [ template ] ;
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
// Add agent configuration
2025-07-06 10:32:39 -05:00
const agentPath = this . convertToWebPath ( dependencies . agent . path , 'bmad-core' ) ;
sections . push ( this . formatSection ( agentPath , dependencies . agent . content , 'bmad-core' ) ) ;
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
// Add all dependencies
for ( const resource of dependencies . resources ) {
2025-07-06 10:32:39 -05:00
const resourcePath = this . convertToWebPath ( resource . path , 'bmad-core' ) ;
sections . push ( this . formatSection ( resourcePath , resource . content , 'bmad-core' ) ) ;
2025-06-10 21:41:58 -05:00
}
2025-06-15 07:59:25 -07:00
2025-08-16 19:08:39 -05:00
return sections . join ( '\n' ) ;
2025-06-10 21:41:58 -05:00
}
async buildTeamBundle ( teamId ) {
const dependencies = await this . resolver . resolveTeamDependencies ( teamId ) ;
2025-07-06 10:32:39 -05:00
const template = this . generateWebInstructions ( 'team' ) ;
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
const sections = [ template ] ;
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
// Add team configuration
2025-07-06 10:32:39 -05:00
const teamPath = this . convertToWebPath ( dependencies . team . path , 'bmad-core' ) ;
sections . push ( this . formatSection ( teamPath , dependencies . team . content , 'bmad-core' ) ) ;
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
// Add all agents
for ( const agent of dependencies . agents ) {
2025-07-06 10:32:39 -05:00
const agentPath = this . convertToWebPath ( agent . path , 'bmad-core' ) ;
sections . push ( this . formatSection ( agentPath , agent . content , 'bmad-core' ) ) ;
2025-06-10 21:41:58 -05:00
}
2025-06-15 07:59:25 -07:00
2025-06-10 21:41:58 -05:00
// Add all deduplicated resources
for ( const resource of dependencies . resources ) {
2025-07-06 10:32:39 -05:00
const resourcePath = this . convertToWebPath ( resource . path , 'bmad-core' ) ;
sections . push ( this . formatSection ( resourcePath , resource . content , 'bmad-core' ) ) ;
2025-06-10 21:41:58 -05:00
}
2025-06-15 07:59:25 -07:00
2025-08-16 19:08:39 -05:00
return sections . join ( '\n' ) ;
2025-06-10 21:41:58 -05:00
}
2025-06-19 13:21:26 -05:00
processAgentContent ( content ) {
// First, replace content before YAML with the template
2025-08-16 19:08:39 -05:00
const yamlContent = yamlUtilities . extractYamlFromAgent ( content ) ;
2025-07-02 21:08:43 -05:00
if ( ! yamlContent ) return content ;
2025-06-19 13:21:26 -05:00
const yamlMatch = content . match ( /```ya?ml\n([\s\S]*?)\n```/ ) ;
if ( ! yamlMatch ) return content ;
2025-08-16 19:08:39 -05:00
2025-06-19 13:21:26 -05:00
const yamlStartIndex = content . indexOf ( yamlMatch [ 0 ] ) ;
const yamlEndIndex = yamlStartIndex + yamlMatch [ 0 ] . length ;
2025-06-27 23:38:34 -05:00
2025-06-19 13:21:26 -05:00
// Parse YAML and remove root and IDE-FILE-RESOLUTION properties
try {
2025-08-16 19:08:39 -05:00
const yaml = require ( 'js-yaml' ) ;
2025-06-19 13:21:26 -05:00
const parsed = yaml . load ( yamlContent ) ;
2025-06-27 23:38:34 -05:00
2025-06-19 13:21:26 -05:00
// Remove the properties if they exist at root level
delete parsed . root ;
2025-08-16 19:08:39 -05:00
delete parsed [ 'IDE-FILE-RESOLUTION' ] ;
delete parsed [ 'REQUEST-RESOLUTION' ] ;
2025-06-27 23:38:34 -05:00
2025-06-19 13:21:26 -05:00
// Also remove from activation-instructions if they exist
2025-08-16 19:08:39 -05:00
if ( parsed [ 'activation-instructions' ] && Array . isArray ( parsed [ 'activation-instructions' ] ) ) {
parsed [ 'activation-instructions' ] = parsed [ 'activation-instructions' ] . filter (
2025-06-27 23:38:34 -05:00
( instruction ) => {
return (
2025-07-06 19:39:34 -05:00
typeof instruction === 'string' &&
2025-08-16 19:08:39 -05:00
! instruction . startsWith ( 'IDE-FILE-RESOLUTION:' ) &&
! instruction . startsWith ( 'REQUEST-RESOLUTION:' )
2025-06-27 23:38:34 -05:00
) ;
2025-08-16 19:08:39 -05:00
} ,
2025-06-27 23:38:34 -05:00
) ;
2025-06-19 13:21:26 -05:00
}
2025-06-27 23:38:34 -05:00
2025-06-19 13:21:26 -05:00
// Reconstruct the YAML
const cleanedYaml = yaml . dump ( parsed , { lineWidth : - 1 } ) ;
2025-06-27 23:38:34 -05:00
2025-06-19 13:21:26 -05:00
// Get the agent name from the YAML for the header
2025-08-16 19:08:39 -05:00
const agentName = parsed . agent ? . id || 'agent' ;
2025-06-27 23:38:34 -05:00
2025-06-19 13:21:26 -05:00
// Build the new content with just the agent header and YAML
2025-07-04 11:53:57 -05:00
const newHeader = ` # ${ agentName } \n \n CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode: \n \n ` ;
2025-08-16 19:08:39 -05:00
const afterYaml = content . slice ( Math . max ( 0 , yamlEndIndex ) ) ;
2025-06-27 23:38:34 -05:00
2025-08-16 19:08:39 -05:00
return newHeader + '```yaml\n' + cleanedYaml . trim ( ) + '\n```' + afterYaml ;
2025-06-19 13:21:26 -05:00
} catch ( error ) {
2025-08-16 19:08:39 -05:00
console . warn ( 'Failed to process agent YAML:' , error . message ) ;
2025-06-19 13:21:26 -05:00
// If parsing fails, return original content
return content ;
}
}
2025-07-06 10:32:39 -05:00
formatSection ( path , content , bundleRoot = 'bmad-core' ) {
2025-08-16 19:08:39 -05:00
const separator = '====================' ;
2025-06-27 23:38:34 -05:00
2025-06-19 13:21:26 -05:00
// Process agent content if this is an agent file
2025-08-16 19:08:39 -05:00
if ( path . includes ( '/agents/' ) ) {
2025-06-19 13:21:26 -05:00
content = this . processAgentContent ( content ) ;
}
2025-06-27 23:38:34 -05:00
2025-07-06 10:32:39 -05:00
// Replace {root} references with the actual bundle root
content = this . replaceRootReferences ( content , bundleRoot ) ;
2025-06-10 21:41:58 -05:00
return [
` ${ separator } START: ${ path } ${ separator } ` ,
content . trim ( ) ,
` ${ separator } END: ${ path } ${ separator } ` ,
2025-08-16 19:08:39 -05:00
'' ,
] . join ( '\n' ) ;
2025-06-10 21:41:58 -05:00
}
2025-07-06 10:32:39 -05:00
replaceRootReferences ( content , bundleRoot ) {
// Replace {root} with the appropriate bundle root path
2025-08-16 19:08:39 -05:00
return content . replaceAll ( '{root}' , ` . ${ bundleRoot } ` ) ;
2025-07-06 10:32:39 -05:00
}
2025-06-10 21:41:58 -05:00
async validate ( ) {
2025-08-16 19:08:39 -05:00
console . log ( 'Validating agent configurations...' ) ;
2025-06-10 21:41:58 -05:00
const agents = await this . resolver . listAgents ( ) ;
for ( const agentId of agents ) {
try {
await this . resolver . resolveAgentDependencies ( agentId ) ;
console . log ( ` ✓ ${ agentId } ` ) ;
} catch ( error ) {
console . log ( ` ✗ ${ agentId } : ${ error . message } ` ) ;
throw error ;
}
}
2025-08-16 19:08:39 -05:00
console . log ( '\nValidating team configurations...' ) ;
2025-06-10 21:41:58 -05:00
const teams = await this . resolver . listTeams ( ) ;
for ( const teamId of teams ) {
try {
await this . resolver . resolveTeamDependencies ( teamId ) ;
console . log ( ` ✓ ${ teamId } ` ) ;
} catch ( error ) {
console . log ( ` ✗ ${ teamId } : ${ error . message } ` ) ;
throw error ;
}
}
}
2025-06-16 18:34:12 -05:00
async buildAllExpansionPacks ( options = { } ) {
const expansionPacks = await this . listExpansionPacks ( ) ;
for ( const packName of expansionPacks ) {
console . log ( ` Building expansion pack: ${ packName } ` ) ;
await this . buildExpansionPack ( packName , options ) ;
}
console . log ( ` Built ${ expansionPacks . length } expansion pack bundles ` ) ;
}
async buildExpansionPack ( packName , options = { } ) {
2025-08-16 19:08:39 -05:00
const packDir = path . join ( this . rootDir , 'expansion-packs' , packName ) ;
const outputDirectories = [ path . join ( this . rootDir , 'dist' , 'expansion-packs' , packName ) ] ;
2025-06-16 18:34:12 -05:00
// Clean output directories if requested
if ( options . clean !== false ) {
2025-08-16 19:08:39 -05:00
for ( const outputDir of outputDirectories ) {
2025-06-16 18:34:12 -05:00
try {
await fs . rm ( outputDir , { recursive : true , force : true } ) ;
2025-08-16 19:08:39 -05:00
} catch {
2025-06-16 18:34:12 -05:00
// Directory might not exist, that's fine
}
}
}
// Build individual agents first
2025-08-16 19:08:39 -05:00
const agentsDir = path . join ( packDir , 'agents' ) ;
2025-06-16 18:34:12 -05:00
try {
const agentFiles = await fs . readdir ( agentsDir ) ;
2025-08-16 19:08:39 -05:00
const agentMarkdownFiles = agentFiles . filter ( ( f ) => f . endsWith ( '.md' ) ) ;
2025-06-19 12:55:16 -05:00
2025-06-16 18:34:12 -05:00
if ( agentMarkdownFiles . length > 0 ) {
console . log ( ` Building individual agents for ${ packName } : ` ) ;
2025-06-19 12:55:16 -05:00
2025-06-16 18:34:12 -05:00
for ( const agentFile of agentMarkdownFiles ) {
2025-08-16 19:08:39 -05:00
const agentName = agentFile . replace ( '.md' , '' ) ;
2025-06-16 18:34:12 -05:00
console . log ( ` - ${ agentName } ` ) ;
2025-06-19 12:55:16 -05:00
2025-06-16 18:34:12 -05:00
// Build individual agent bundle
const bundle = await this . buildExpansionAgentBundle ( packName , packDir , agentName ) ;
2025-06-19 12:55:16 -05:00
2025-06-16 18:34:12 -05:00
// Write to all output directories
2025-08-16 19:08:39 -05:00
for ( const outputDir of outputDirectories ) {
const agentsOutputDir = path . join ( outputDir , 'agents' ) ;
2025-06-16 18:34:12 -05:00
await fs . mkdir ( agentsOutputDir , { recursive : true } ) ;
const outputFile = path . join ( agentsOutputDir , ` ${ agentName } .txt ` ) ;
2025-08-16 19:08:39 -05:00
await fs . writeFile ( outputFile , bundle , 'utf8' ) ;
2025-06-16 18:34:12 -05:00
}
}
}
2025-08-16 19:08:39 -05:00
} catch {
2025-06-16 18:34:12 -05:00
console . debug ( ` No agents directory found for ${ packName } ` ) ;
}
// Build team bundle
2025-08-16 19:08:39 -05:00
const agentTeamsDir = path . join ( packDir , 'agent-teams' ) ;
2025-06-16 18:34:12 -05:00
try {
const teamFiles = await fs . readdir ( agentTeamsDir ) ;
2025-08-16 19:08:39 -05:00
const teamFile = teamFiles . find ( ( f ) => f . endsWith ( '.yaml' ) ) ;
2025-06-19 12:55:16 -05:00
2025-06-16 18:34:12 -05:00
if ( teamFile ) {
console . log ( ` Building team bundle for ${ packName } ` ) ;
const teamConfigPath = path . join ( agentTeamsDir , teamFile ) ;
2025-06-19 12:55:16 -05:00
2025-06-16 18:34:12 -05:00
// Build expansion pack as a team bundle
const bundle = await this . buildExpansionTeamBundle ( packName , packDir , teamConfigPath ) ;
2025-06-19 12:55:16 -05:00
2025-06-16 18:34:12 -05:00
// Write to all output directories
2025-08-16 19:08:39 -05:00
for ( const outputDir of outputDirectories ) {
const teamsOutputDir = path . join ( outputDir , 'teams' ) ;
2025-06-16 18:34:12 -05:00
await fs . mkdir ( teamsOutputDir , { recursive : true } ) ;
2025-08-16 19:08:39 -05:00
const outputFile = path . join ( teamsOutputDir , teamFile . replace ( '.yaml' , '.txt' ) ) ;
await fs . writeFile ( outputFile , bundle , 'utf8' ) ;
2025-06-16 18:34:12 -05:00
console . log ( ` ✓ Created bundle: ${ path . relative ( this . rootDir , outputFile ) } ` ) ;
}
} else {
console . warn ( ` ⚠ No team configuration found in ${ packName } /agent-teams/ ` ) ;
}
2025-08-16 19:08:39 -05:00
} catch {
2025-06-16 18:34:12 -05:00
console . warn ( ` ⚠ No agent-teams directory found for ${ packName } ` ) ;
}
}
async buildExpansionAgentBundle ( packName , packDir , agentName ) {
2025-07-06 10:32:39 -05:00
const template = this . generateWebInstructions ( 'expansion-agent' , packName ) ;
2025-06-16 18:34:12 -05:00
const sections = [ template ] ;
// Add agent configuration
2025-08-16 19:08:39 -05:00
const agentPath = path . join ( packDir , 'agents' , ` ${ agentName } .md ` ) ;
const agentContent = await fs . readFile ( agentPath , 'utf8' ) ;
2025-07-06 10:32:39 -05:00
const agentWebPath = this . convertToWebPath ( agentPath , packName ) ;
sections . push ( this . formatSection ( agentWebPath , agentContent , packName ) ) ;
2025-06-16 18:34:12 -05:00
2025-06-18 19:17:09 -05:00
// Resolve and add agent dependencies
2025-08-16 19:08:39 -05:00
const yamlContent = yamlUtilities . extractYamlFromAgent ( agentContent ) ;
2025-07-02 21:08:43 -05:00
if ( yamlContent ) {
2025-06-16 18:34:12 -05:00
try {
2025-08-16 19:08:39 -05:00
const yaml = require ( 'js-yaml' ) ;
2025-07-02 21:08:43 -05:00
const agentConfig = yaml . load ( yamlContent ) ;
2025-06-19 12:55:16 -05:00
2025-06-16 18:34:12 -05:00
if ( agentConfig . dependencies ) {
2025-06-18 19:17:09 -05:00
// Add resources, first try expansion pack, then core
2025-06-16 18:34:12 -05:00
for ( const [ resourceType , resources ] of Object . entries ( agentConfig . dependencies ) ) {
if ( Array . isArray ( resources ) ) {
for ( const resourceName of resources ) {
2025-06-18 19:17:09 -05:00
let found = false ;
2025-06-19 12:55:16 -05:00
2025-06-18 19:17:09 -05:00
// Try expansion pack first
2025-07-06 19:39:34 -05:00
const resourcePath = path . join ( packDir , resourceType , resourceName ) ;
try {
2025-08-16 19:08:39 -05:00
const resourceContent = await fs . readFile ( resourcePath , 'utf8' ) ;
2025-07-06 19:39:34 -05:00
const resourceWebPath = this . convertToWebPath ( resourcePath , packName ) ;
2025-08-16 19:08:39 -05:00
sections . push ( this . formatSection ( resourceWebPath , resourceContent , packName ) ) ;
2025-07-06 19:39:34 -05:00
found = true ;
2025-08-16 19:08:39 -05:00
} catch {
2025-07-06 19:39:34 -05:00
// Not in expansion pack, continue
2025-06-18 19:17:09 -05:00
}
2025-06-19 12:55:16 -05:00
2025-06-18 19:17:09 -05:00
// If not found in expansion pack, try core
if ( ! found ) {
2025-08-16 19:08:39 -05:00
const corePath = path . join ( this . rootDir , 'bmad-core' , resourceType , resourceName ) ;
2025-07-06 19:39:34 -05:00
try {
2025-08-16 19:08:39 -05:00
const coreContent = await fs . readFile ( corePath , 'utf8' ) ;
2025-07-06 19:39:34 -05:00
const coreWebPath = this . convertToWebPath ( corePath , packName ) ;
2025-08-16 19:08:39 -05:00
sections . push ( this . formatSection ( coreWebPath , coreContent , packName ) ) ;
2025-07-06 19:39:34 -05:00
found = true ;
2025-08-16 19:08:39 -05:00
} catch {
2025-07-06 19:39:34 -05:00
// Not in core either, continue
2025-06-18 19:17:09 -05:00
}
}
2025-06-19 12:55:16 -05:00
2025-06-27 23:38:34 -05:00
// If not found in core, try common folder
if ( ! found ) {
2025-08-16 19:08:39 -05:00
const commonPath = path . join ( this . rootDir , 'common' , resourceType , resourceName ) ;
2025-07-06 19:39:34 -05:00
try {
2025-08-16 19:08:39 -05:00
const commonContent = await fs . readFile ( commonPath , 'utf8' ) ;
2025-07-06 19:39:34 -05:00
const commonWebPath = this . convertToWebPath ( commonPath , packName ) ;
2025-08-16 19:08:39 -05:00
sections . push ( this . formatSection ( commonWebPath , commonContent , packName ) ) ;
2025-07-06 19:39:34 -05:00
found = true ;
2025-08-16 19:08:39 -05:00
} catch {
2025-07-06 19:39:34 -05:00
// Not in common either, continue
2025-06-27 23:38:34 -05:00
}
}
2025-06-18 19:17:09 -05:00
if ( ! found ) {
2025-06-19 12:55:16 -05:00
console . warn (
2025-08-16 19:08:39 -05:00
` ⚠ Dependency ${ resourceType } # ${ resourceName } not found in expansion pack or core ` ,
2025-06-19 12:55:16 -05:00
) ;
2025-06-16 18:34:12 -05:00
}
}
}
}
}
} catch ( error ) {
console . debug ( ` Failed to parse agent YAML for ${ agentName } : ` , error . message ) ;
}
}
2025-08-16 19:08:39 -05:00
return sections . join ( '\n' ) ;
2025-06-16 18:34:12 -05:00
}
async buildExpansionTeamBundle ( packName , packDir , teamConfigPath ) {
2025-07-06 10:32:39 -05:00
const template = this . generateWebInstructions ( 'expansion-team' , packName ) ;
2025-06-16 18:34:12 -05:00
const sections = [ template ] ;
2025-06-19 01:07:21 +02:00
// Add team configuration and parse to get agent list
2025-08-16 19:08:39 -05:00
const teamContent = await fs . readFile ( teamConfigPath , 'utf8' ) ;
const teamFileName = path . basename ( teamConfigPath , '.yaml' ) ;
2025-06-19 01:07:21 +02:00
const teamConfig = this . parseYaml ( teamContent ) ;
2025-07-06 10:32:39 -05:00
const teamWebPath = this . convertToWebPath ( teamConfigPath , packName ) ;
sections . push ( this . formatSection ( teamWebPath , teamContent , packName ) ) ;
2025-06-16 18:34:12 -05:00
2025-06-19 01:07:21 +02:00
// Get list of expansion pack agents
const expansionAgents = new Set ( ) ;
2025-08-16 19:08:39 -05:00
const agentsDir = path . join ( packDir , 'agents' ) ;
2025-06-16 18:34:12 -05:00
try {
const agentFiles = await fs . readdir ( agentsDir ) ;
2025-08-16 19:08:39 -05:00
for ( const agentFile of agentFiles . filter ( ( f ) => f . endsWith ( '.md' ) ) ) {
const agentName = agentFile . replace ( '.md' , '' ) ;
2025-06-19 01:07:21 +02:00
expansionAgents . add ( agentName ) ;
2025-06-16 18:34:12 -05:00
}
2025-08-16 19:08:39 -05:00
} catch {
2025-06-16 18:34:12 -05:00
console . warn ( ` ⚠ No agents directory found in ${ packName } ` ) ;
}
2025-06-18 19:17:09 -05:00
// Build a map of all available expansion pack resources for override checking
const expansionResources = new Map ( ) ;
2025-08-16 19:08:39 -05:00
const resourceDirectories = [ 'templates' , 'tasks' , 'checklists' , 'workflows' , 'data' ] ;
for ( const resourceDir of resourceDirectories ) {
2025-06-18 19:17:09 -05:00
const resourcePath = path . join ( packDir , resourceDir ) ;
try {
const resourceFiles = await fs . readdir ( resourcePath ) ;
2025-06-19 12:55:16 -05:00
for ( const resourceFile of resourceFiles . filter (
2025-08-16 19:08:39 -05:00
( f ) => f . endsWith ( '.md' ) || f . endsWith ( '.yaml' ) ,
2025-06-19 12:55:16 -05:00
) ) {
2025-07-06 19:39:34 -05:00
expansionResources . set ( ` ${ resourceDir } # ${ resourceFile } ` , true ) ;
2025-06-18 19:17:09 -05:00
}
2025-08-16 19:08:39 -05:00
} catch {
2025-06-18 19:17:09 -05:00
// Directory might not exist, that's fine
}
}
2025-06-19 01:07:21 +02:00
// Process all agents listed in team configuration
const agentsToProcess = teamConfig . agents || [ ] ;
2025-06-19 12:55:16 -05:00
2025-06-19 01:07:21 +02:00
// Ensure bmad-orchestrator is always included for teams
2025-08-16 19:08:39 -05:00
if ( ! agentsToProcess . includes ( 'bmad-orchestrator' ) ) {
2025-06-19 01:07:21 +02:00
console . warn ( ` ⚠ Team ${ teamFileName } missing bmad-orchestrator, adding automatically ` ) ;
2025-08-16 19:08:39 -05:00
agentsToProcess . unshift ( 'bmad-orchestrator' ) ;
2025-06-19 01:07:21 +02:00
}
2025-06-18 19:17:09 -05:00
// Track all dependencies from all agents (deduplicated)
const allDependencies = new Map ( ) ;
2025-06-19 01:07:21 +02:00
for ( const agentId of agentsToProcess ) {
if ( expansionAgents . has ( agentId ) ) {
// Use expansion pack version (override)
const agentPath = path . join ( agentsDir , ` ${ agentId } .md ` ) ;
2025-08-16 19:08:39 -05:00
const agentContent = await fs . readFile ( agentPath , 'utf8' ) ;
2025-07-06 10:32:39 -05:00
const expansionAgentWebPath = this . convertToWebPath ( agentPath , packName ) ;
sections . push ( this . formatSection ( expansionAgentWebPath , agentContent , packName ) ) ;
2025-06-18 19:17:09 -05:00
// Parse and collect dependencies from expansion agent
const agentYaml = agentContent . match ( /```yaml\n([\s\S]*?)\n```/ ) ;
if ( agentYaml ) {
try {
const agentConfig = this . parseYaml ( agentYaml [ 1 ] ) ;
if ( agentConfig . dependencies ) {
for ( const [ resourceType , resources ] of Object . entries ( agentConfig . dependencies ) ) {
if ( Array . isArray ( resources ) ) {
for ( const resourceName of resources ) {
const key = ` ${ resourceType } # ${ resourceName } ` ;
if ( ! allDependencies . has ( key ) ) {
allDependencies . set ( key , { type : resourceType , name : resourceName } ) ;
}
}
}
}
}
} catch ( error ) {
console . debug ( ` Failed to parse agent YAML for ${ agentId } : ` , error . message ) ;
}
}
2025-06-19 01:07:21 +02:00
} else {
2025-07-04 07:47:57 -05:00
// Use core BMad version
2025-06-19 01:07:21 +02:00
try {
2025-08-16 19:08:39 -05:00
const coreAgentPath = path . join ( this . rootDir , 'bmad-core' , 'agents' , ` ${ agentId } .md ` ) ;
const coreAgentContent = await fs . readFile ( coreAgentPath , 'utf8' ) ;
2025-07-06 10:32:39 -05:00
const coreAgentWebPath = this . convertToWebPath ( coreAgentPath , packName ) ;
sections . push ( this . formatSection ( coreAgentWebPath , coreAgentContent , packName ) ) ;
2025-06-18 19:17:09 -05:00
// Parse and collect dependencies from core agent
2025-08-16 19:08:39 -05:00
const yamlContent = yamlUtilities . extractYamlFromAgent ( coreAgentContent , true ) ;
2025-07-02 21:08:43 -05:00
if ( yamlContent ) {
2025-06-18 19:17:09 -05:00
try {
const agentConfig = this . parseYaml ( yamlContent ) ;
if ( agentConfig . dependencies ) {
for ( const [ resourceType , resources ] of Object . entries ( agentConfig . dependencies ) ) {
if ( Array . isArray ( resources ) ) {
for ( const resourceName of resources ) {
const key = ` ${ resourceType } # ${ resourceName } ` ;
if ( ! allDependencies . has ( key ) ) {
allDependencies . set ( key , { type : resourceType , name : resourceName } ) ;
}
}
}
}
}
} catch ( error ) {
console . debug ( ` Failed to parse agent YAML for ${ agentId } : ` , error . message ) ;
}
}
2025-08-16 19:08:39 -05:00
} catch {
2025-06-19 01:07:21 +02:00
console . warn ( ` ⚠ Agent ${ agentId } not found in core or expansion pack ` ) ;
}
}
}
2025-06-18 19:17:09 -05:00
// Add all collected dependencies from agents
// Always prefer expansion pack versions if they exist
for ( const [ key , dep ] of allDependencies ) {
let found = false ;
2025-06-19 12:55:16 -05:00
2025-06-18 19:17:09 -05:00
// Always check expansion pack first, even if the dependency came from a core agent
if ( expansionResources . has ( key ) ) {
// We know it exists in expansion pack, find and load it
2025-07-06 19:39:34 -05:00
const expansionPath = path . join ( packDir , dep . type , dep . name ) ;
try {
2025-08-16 19:08:39 -05:00
const content = await fs . readFile ( expansionPath , 'utf8' ) ;
2025-07-06 19:39:34 -05:00
const expansionWebPath = this . convertToWebPath ( expansionPath , packName ) ;
sections . push ( this . formatSection ( expansionWebPath , content , packName ) ) ;
console . log ( ` ✓ Using expansion override for ${ key } ` ) ;
found = true ;
2025-08-16 19:08:39 -05:00
} catch {
2025-07-06 19:39:34 -05:00
// Try next extension
2025-06-18 19:17:09 -05:00
}
}
2025-06-19 12:55:16 -05:00
2025-06-18 19:17:09 -05:00
// If not found in expansion pack (or doesn't exist there), try core
if ( ! found ) {
2025-08-16 19:08:39 -05:00
const corePath = path . join ( this . rootDir , 'bmad-core' , dep . type , dep . name ) ;
2025-07-06 19:39:34 -05:00
try {
2025-08-16 19:08:39 -05:00
const content = await fs . readFile ( corePath , 'utf8' ) ;
2025-07-06 19:39:34 -05:00
const coreWebPath = this . convertToWebPath ( corePath , packName ) ;
sections . push ( this . formatSection ( coreWebPath , content , packName ) ) ;
found = true ;
2025-08-16 19:08:39 -05:00
} catch {
2025-07-06 19:39:34 -05:00
// Not in core either, continue
2025-06-18 19:17:09 -05:00
}
}
2025-06-19 12:55:16 -05:00
2025-06-27 23:38:34 -05:00
// If not found in core, try common folder
if ( ! found ) {
2025-08-16 19:08:39 -05:00
const commonPath = path . join ( this . rootDir , 'common' , dep . type , dep . name ) ;
2025-07-06 19:39:34 -05:00
try {
2025-08-16 19:08:39 -05:00
const content = await fs . readFile ( commonPath , 'utf8' ) ;
2025-07-06 19:39:34 -05:00
const commonWebPath = this . convertToWebPath ( commonPath , packName ) ;
sections . push ( this . formatSection ( commonWebPath , content , packName ) ) ;
found = true ;
2025-08-16 19:08:39 -05:00
} catch {
2025-07-06 19:39:34 -05:00
// Not in common either, continue
2025-06-27 23:38:34 -05:00
}
}
2025-06-18 19:17:09 -05:00
if ( ! found ) {
console . warn ( ` ⚠ Dependency ${ key } not found in expansion pack or core ` ) ;
}
}
// Add remaining expansion pack resources not already included as dependencies
2025-08-16 19:08:39 -05:00
for ( const resourceDir of resourceDirectories ) {
2025-06-16 18:34:12 -05:00
const resourcePath = path . join ( packDir , resourceDir ) ;
try {
const resourceFiles = await fs . readdir ( resourcePath ) ;
2025-06-19 12:55:16 -05:00
for ( const resourceFile of resourceFiles . filter (
2025-08-16 19:08:39 -05:00
( f ) => f . endsWith ( '.md' ) || f . endsWith ( '.yaml' ) ,
2025-06-19 12:55:16 -05:00
) ) {
2025-06-16 18:34:12 -05:00
const filePath = path . join ( resourcePath , resourceFile ) ;
2025-08-16 19:08:39 -05:00
const fileContent = await fs . readFile ( filePath , 'utf8' ) ;
const fileName = resourceFile . replace ( /\.(md|yaml)$/ , '' ) ;
2025-06-19 12:55:16 -05:00
2025-06-18 19:17:09 -05:00
// Only add if not already included as a dependency
const resourceKey = ` ${ resourceDir } # ${ fileName } ` ;
if ( ! allDependencies . has ( resourceKey ) ) {
2025-07-06 10:32:39 -05:00
const fullResourcePath = path . join ( resourcePath , resourceFile ) ;
const resourceWebPath = this . convertToWebPath ( fullResourcePath , packName ) ;
sections . push ( this . formatSection ( resourceWebPath , fileContent , packName ) ) ;
2025-06-18 19:17:09 -05:00
}
2025-06-16 18:34:12 -05:00
}
2025-08-16 19:08:39 -05:00
} catch {
2025-06-16 18:34:12 -05:00
// Directory might not exist, that's fine
}
}
2025-08-16 19:08:39 -05:00
return sections . join ( '\n' ) ;
2025-06-16 18:34:12 -05:00
}
async listExpansionPacks ( ) {
2025-08-16 19:08:39 -05:00
const expansionPacksDir = path . join ( this . rootDir , 'expansion-packs' ) ;
2025-06-16 18:34:12 -05:00
try {
const entries = await fs . readdir ( expansionPacksDir , { withFileTypes : true } ) ;
2025-06-19 12:55:16 -05:00
return entries . filter ( ( entry ) => entry . isDirectory ( ) ) . map ( ( entry ) => entry . name ) ;
2025-08-16 19:08:39 -05:00
} catch {
console . warn ( 'No expansion-packs directory found' ) ;
2025-06-16 18:34:12 -05:00
return [ ] ;
}
}
2025-06-10 21:41:58 -05:00
listAgents ( ) {
return this . resolver . listAgents ( ) ;
}
}
2025-06-19 12:55:16 -05:00
module . exports = WebBuilder ;