const path = require('node:path'); const fs = require('fs-extra'); const chalk = require('chalk'); const yaml = require('js-yaml'); const { DependencyResolver } = require('../installers/lib/core/dependency-resolver'); const { XmlHandler } = require('../lib/xml-handler'); const { AgentPartyGenerator } = require('../lib/agent-party-generator'); const xml2js = require('xml2js'); const { getProjectRoot, getSourcePath, getModulePath } = require('../lib/project-root'); class WebBundler { constructor(sourceDir = null, outputDir = 'web-bundles') { this.sourceDir = sourceDir || getSourcePath(); this.outputDir = path.isAbsolute(outputDir) ? outputDir : path.join(getProjectRoot(), outputDir); this.modulesPath = getSourcePath('modules'); this.utilityPath = getSourcePath('utility'); this.dependencyResolver = new DependencyResolver(); this.xmlHandler = new XmlHandler(); // Cache for resolved dependencies to avoid duplicates this.dependencyCache = new Map(); // Discovered agents and teams for manifest generation this.discoveredAgents = []; this.discoveredTeams = []; // Temporary directory for generated manifests this.tempDir = path.join(process.cwd(), '.bundler-temp'); this.tempManifestDir = path.join(this.tempDir, 'bmad', '_cfg'); // Bundle statistics this.stats = { totalAgents: 0, bundledAgents: 0, skippedAgents: 0, failedAgents: 0, invalidXml: 0, warnings: [], }; } /** * Main entry point to bundle all modules */ async bundleAll() { console.log(chalk.cyan.bold('═══════════════════════════════════════════════')); console.log(chalk.cyan.bold(' 🚀 Web Bundle Generation')); console.log(chalk.cyan.bold('═══════════════════════════════════════════════\n')); try { // Pre-discover all modules to generate complete manifests const modules = await this.discoverModules(); for (const module of modules) { await this.preDiscoverModule(module); } // Create temporary manifest files await this.createTempManifests(); // Process all modules for (const module of modules) { await this.bundleModule(module); } // Display summary this.displaySummary(); } finally { // Clean up temp files await this.cleanupTempFiles(); } } /** * Bundle a specific module */ async bundleModule(moduleName) { const modulePath = path.join(this.modulesPath, moduleName); if (!(await fs.pathExists(modulePath))) { console.log(chalk.yellow(`Module ${moduleName} not found`)); return { module: moduleName, agents: [], teams: [] }; } console.log(chalk.bold(`\n📦 Bundling module: ${moduleName}`)); const results = { module: moduleName, agents: [], teams: [], }; // Pre-discover all agents and teams for manifest generation await this.preDiscoverModule(moduleName); // Ensure temp manifests exist (might not exist if called directly) if (!(await fs.pathExists(this.tempManifestDir))) { await this.createTempManifests(); } // Process agents const agents = await this.discoverAgents(modulePath); for (const agent of agents) { try { await this.bundleAgent(moduleName, agent, false); // false = don't track again results.agents.push(agent); } catch (error) { console.error(` Failed to bundle agent ${agent}:`, error.message); } } // Process teams (Phase 4 - to be implemented) // const teams = await this.discoverTeams(modulePath); // for (const team of teams) { // try { // await this.bundleTeam(moduleName, team); // results.teams.push(team); // } catch (error) { // console.error(` Failed to bundle team ${team}:`, error.message); // } // } return results; } /** * Bundle a single agent */ async bundleAgent(moduleName, agentFile, shouldTrack = true) { const agentName = path.basename(agentFile, '.md'); this.stats.totalAgents++; console.log(chalk.dim(` → Processing: ${agentName}`)); const agentPath = path.join(this.modulesPath, moduleName, 'agents', agentFile); // Check if agent file exists if (!(await fs.pathExists(agentPath))) { this.stats.failedAgents++; console.log(chalk.red(` ✗ Agent file not found`)); throw new Error(`Agent file not found: ${agentPath}`); } // Read agent file const content = await fs.readFile(agentPath, 'utf8'); // Extract agent XML from markdown let agentXml = this.extractAgentXml(content); if (!agentXml) { this.stats.failedAgents++; console.log(chalk.red(` ✗ No agent XML found in ${agentFile}`)); return; } // Check if agent has bundle="false" attribute if (this.shouldSkipBundling(agentXml)) { this.stats.skippedAgents++; console.log(chalk.gray(` ⊘ Skipped (bundle="false")`)); return; } // Process {project-root} references in agent XML agentXml = this.processProjectRootReferences(agentXml); // Track for manifest generation BEFORE generating manifests (if not pre-discovered) if (shouldTrack) { const agentDetails = AgentPartyGenerator.extractAgentDetails(content, moduleName, agentName); if (agentDetails) { this.discoveredAgents.push(agentDetails); } } // Resolve dependencies with warning tracking const dependencyWarnings = []; const { dependencies, skippedWorkflows } = await this.resolveAgentDependencies(agentXml, moduleName, dependencyWarnings); if (dependencyWarnings.length > 0) { this.stats.warnings.push({ agent: agentName, warnings: dependencyWarnings }); } // Remove commands for skipped workflows from agent XML if (skippedWorkflows.length > 0) { agentXml = this.removeSkippedWorkflowCommands(agentXml, skippedWorkflows); } // Build the bundle (no manifests for individual agents) const bundle = this.buildAgentBundle(agentXml, dependencies); // Validate XML const isValid = await this.validateXml(bundle); if (!isValid) { this.stats.invalidXml++; console.log(chalk.red(` ⚠ Invalid XML generated!`)); } // Write bundle to output const outputPath = path.join(this.outputDir, moduleName, 'agents', `${agentName}.xml`); await fs.ensureDir(path.dirname(outputPath)); await fs.writeFile(outputPath, bundle, 'utf8'); this.stats.bundledAgents++; const statusIcon = isValid ? chalk.green('✓') : chalk.yellow('⚠'); console.log(` ${statusIcon} Bundled: ${agentName}.xml${isValid ? '' : chalk.yellow(' (invalid XML)')}`); } /** * Pre-discover all agents and teams in a module for manifest generation */ async preDiscoverModule(moduleName) { const modulePath = path.join(this.modulesPath, moduleName); // Clear any previously discovered agents for this module this.discoveredAgents = this.discoveredAgents.filter((a) => a.module !== moduleName); // Discover agents const agentsPath = path.join(modulePath, 'agents'); if (await fs.pathExists(agentsPath)) { const files = await fs.readdir(agentsPath); for (const file of files) { if (file.endsWith('.md') && !file.toLowerCase().includes('readme')) { const agentPath = path.join(agentsPath, file); const content = await fs.readFile(agentPath, 'utf8'); const agentXml = this.extractAgentXml(content); if (agentXml) { // Skip agents with bundle="false" if (this.shouldSkipBundling(agentXml)) { continue; } const agentName = path.basename(file, '.md'); // Use the shared generator to extract agent details (pass full content) const agentDetails = AgentPartyGenerator.extractAgentDetails(content, moduleName, agentName); if (agentDetails) { this.discoveredAgents.push(agentDetails); } } } } } // TODO: Discover teams when implemented } /** * Extract agent XML from markdown content */ extractAgentXml(content) { // Try 4 backticks first (can contain 3 backtick blocks inside) let match = content.match(/````xml\s*([\s\S]*?)````/); if (!match) { // Fall back to 3 backticks if no 4-backtick block found match = content.match(/```xml\s*([\s\S]*?)```/); } if (match) { const xmlContent = match[1]; const agentMatch = xmlContent.match(/]*>[\s\S]*?<\/agent>/); return agentMatch ? agentMatch[0] : null; } // Fall back to direct extraction match = content.match(/]*>[\s\S]*?<\/agent>/); return match ? match[0] : null; } /** * Resolve all dependencies for an agent */ async resolveAgentDependencies(agentXml, moduleName, warnings = []) { const dependencies = new Map(); const processed = new Set(); const skippedWorkflows = []; // Extract file references from agent XML const { refs, workflowRefs } = this.extractFileReferences(agentXml); // Process regular file references for (const ref of refs) { await this.processFileDependency(ref, dependencies, processed, moduleName, warnings); } // Process workflow references with special handling for (const workflowRef of workflowRefs) { const result = await this.processWorkflowDependency(workflowRef, dependencies, processed, moduleName, warnings); if (result && result.skipped) { skippedWorkflows.push(workflowRef); } } return { dependencies, skippedWorkflows }; } /** * Extract file references from agent XML */ extractFileReferences(xml) { const refs = new Set(); const workflowRefs = new Set(); // Match various file reference patterns const patterns = [ /exec="([^"]+)"/g, // Command exec paths /tmpl="([^"]+)"/g, // Template paths /data="([^"]+)"/g, // Data file paths /file="([^"]+)"/g, // Generic file refs /src="([^"]+)"/g, // Source paths /system-prompts="([^"]+)"/g, /tools="([^"]+)"/g, /workflows="([^"]+)"/g, /knowledge="([^"]+)"/g, /{project-root}\/([^"'\s<>]+)/g, ]; for (const pattern of patterns) { let match; while ((match = pattern.exec(xml)) !== null) { let filePath = match[1]; // Remove {project-root} prefix if present filePath = filePath.replace(/^{project-root}\//, ''); if (filePath) { refs.add(filePath); } } } // Extract run-workflow references (special handling for workflows) const workflowPattern = /run-workflow="([^"]+)"/g; let workflowMatch; while ((workflowMatch = workflowPattern.exec(xml)) !== null) { let workflowPath = workflowMatch[1]; workflowPath = workflowPath.replace(/^{project-root}\//, ''); if (workflowPath) { workflowRefs.add(workflowPath); } } return { refs: [...refs], workflowRefs: [...workflowRefs] }; } /** * Remove commands from agent XML that reference skipped workflows */ removeSkippedWorkflowCommands(agentXml, skippedWorkflows) { let modifiedXml = agentXml; // For each skipped workflow, find and remove the corresponding command for (const workflowPath of skippedWorkflows) { // Match: ... // Need to escape special regex characters in the path const escapedPath = workflowPath.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); // Pattern to match the command line with this workflow const pattern = new RegExp(`\\s*]*>.*?\\s*`, 'gs'); modifiedXml = modifiedXml.replace(pattern, ''); } return modifiedXml; } /** * Process a file dependency recursively */ async processFileDependency(filePath, dependencies, processed, moduleName, warnings = []) { // Skip if already processed if (processed.has(filePath)) { return; } processed.add(filePath); // Handle wildcard patterns if (filePath.includes('*')) { await this.processWildcardDependency(filePath, dependencies, processed, moduleName, warnings); return; } // Resolve actual file path const actualPath = this.resolveFilePath(filePath, moduleName); if (!actualPath || !(await fs.pathExists(actualPath))) { warnings.push(filePath); return; } // Read file content let content = await fs.readFile(actualPath, 'utf8'); // Process {project-root} references content = this.processProjectRootReferences(content); // Extract dependencies from frontmatter if present const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); if (frontmatterMatch) { const frontmatter = frontmatterMatch[1]; // Look for dependencies in frontmatter const depMatch = frontmatter.match(/dependencies:\s*\[(.*?)\]/); if (depMatch) { const deps = depMatch[1].match(/['"]([^'"]+)['"]/g); if (deps) { for (const dep of deps) { const depPath = dep.replaceAll(/['"]/g, '').replace(/^{project-root}\//, ''); if (depPath && !processed.has(depPath)) { await this.processFileDependency(depPath, dependencies, processed, moduleName, warnings); } } } } // Look for template references const templateMatch = frontmatter.match(/template:\s*\[(.*?)\]/); if (templateMatch) { const templates = templateMatch[1].match(/['"]([^'"]+)['"]/g); if (templates) { for (const template of templates) { const templatePath = template.replaceAll(/['"]/g, '').replace(/^{project-root}\//, ''); if (templatePath && !processed.has(templatePath)) { await this.processFileDependency(templatePath, dependencies, processed, moduleName, warnings); } } } } } // Extract XML from markdown if applicable const ext = path.extname(actualPath).toLowerCase(); let processedContent = content; switch (ext) { case '.md': { // Try to extract XML from markdown - handle both 3 and 4 backtick blocks // First try 4 backticks (which can contain 3 backtick blocks inside) let xmlMatches = [...content.matchAll(/````xml\s*([\s\S]*?)````/g)]; // If no 4-backtick blocks, try 3 backticks if (xmlMatches.length === 0) { xmlMatches = [...content.matchAll(/```xml\s*([\s\S]*?)```/g)]; } const xmlBlocks = []; for (const match of xmlMatches) { if (match[1]) { xmlBlocks.push(match[1].trim()); } } if (xmlBlocks.length > 0) { // For XML content, just include it directly (it's already valid XML) processedContent = xmlBlocks.join('\n\n'); } else { // No XML blocks found, skip non-XML markdown files return; } break; } case '.csv': { // CSV files need special handling - convert to XML file-index const lines = content.split('\n').filter((line) => line.trim()); if (lines.length === 0) return; const headers = lines[0].split(',').map((h) => h.trim()); const rows = lines.slice(1); const indexParts = [``]; indexParts.push(' '); // Track files referenced in CSV for additional bundling const referencedFiles = new Set(); for (const row of rows) { const values = row.split(',').map((v) => v.trim()); if (values.every((v) => !v)) continue; indexParts.push(' '); for (const [i, header] of headers.entries()) { const value = values[i] || ''; const tagName = header.toLowerCase().replaceAll(/[^a-z0-9]/g, '_'); indexParts.push(` <${tagName}>${value}`); // Track referenced files if (header.toLowerCase().includes('file') && value.endsWith('.md')) { // Build path relative to CSV location const csvDir = path.dirname(actualPath); const refPath = path.join(csvDir, value); if (fs.existsSync(refPath)) { const refId = filePath.replace('index.csv', value); referencedFiles.add(refId); } } } indexParts.push(' '); } indexParts.push(' ', ''); // Store the XML version dependencies.set(filePath, indexParts.join('\n')); // Process referenced files from CSV for (const refId of referencedFiles) { if (!processed.has(refId)) { await this.processFileDependency(refId, dependencies, processed, moduleName, warnings); } } return; } case '.xml': { // XML files can be included directly processedContent = content; break; } default: { // For other non-XML file types, skip them return; } } // Store the processed content dependencies.set(filePath, processedContent); // Recursively scan for more dependencies const { refs: nestedRefs } = this.extractFileReferences(processedContent); for (const ref of nestedRefs) { await this.processFileDependency(ref, dependencies, processed, moduleName, warnings); } } /** * Process a workflow YAML file and its bundle files */ async processWorkflowDependency(workflowPath, dependencies, processed, moduleName, warnings = []) { // Skip if already processed if (processed.has(workflowPath)) { return { skipped: false }; } processed.add(workflowPath); // Resolve actual file path const actualPath = this.resolveFilePath(workflowPath, moduleName); if (!actualPath || !(await fs.pathExists(actualPath))) { warnings.push(workflowPath); return { skipped: true }; } // Read and parse YAML file const yamlContent = await fs.readFile(actualPath, 'utf8'); let workflowConfig; try { workflowConfig = yaml.load(yamlContent); } catch (error) { warnings.push(`${workflowPath} (invalid YAML: ${error.message})`); return { skipped: true }; } // Check if web_bundle is explicitly set to false if (workflowConfig.web_bundle === false) { // Mark this workflow as skipped so we can remove the command from agent return { skipped: true, workflowPath }; } // Create YAML content with only web_bundle section (flattened) let bundleYamlContent; if (workflowConfig.web_bundle && typeof workflowConfig.web_bundle === 'object') { // Only include the web_bundle content, flattened to root level bundleYamlContent = yaml.dump(workflowConfig.web_bundle); } else { // If no web_bundle section, include full YAML bundleYamlContent = yamlContent; } // Include the YAML file with only web_bundle content, wrapped in XML const yamlId = workflowPath.replace(/^{project-root}\//, ''); const wrappedYaml = this.wrapContentInXml(bundleYamlContent, yamlId, 'yaml'); dependencies.set(yamlId, wrappedYaml); // Always include core workflow task when processing workflows await this.includeCoreWorkflowFiles(dependencies, processed, moduleName, warnings); // Check if advanced elicitation is enabled if (workflowConfig.web_bundle && workflowConfig.web_bundle.use_advanced_elicitation) { await this.includeAdvancedElicitationFiles(dependencies, processed, moduleName, warnings); } // Process web_bundle_files if they exist if (workflowConfig.web_bundle && workflowConfig.web_bundle.web_bundle_files) { const bundleFiles = workflowConfig.web_bundle.web_bundle_files; for (const bundleFilePath of bundleFiles) { if (processed.has(bundleFilePath)) { continue; } const bundleActualPath = this.resolveFilePath(bundleFilePath, moduleName); if (!bundleActualPath || !(await fs.pathExists(bundleActualPath))) { warnings.push(bundleFilePath); continue; } // Check if this is another workflow.yaml file - if so, recursively process it if (bundleFilePath.endsWith('workflow.yaml')) { // Recursively process this workflow and its dependencies await this.processWorkflowDependency(bundleFilePath, dependencies, processed, moduleName, warnings); } else { // Regular file - process normally processed.add(bundleFilePath); // Read the file content const fileContent = await fs.readFile(bundleActualPath, 'utf8'); const fileExt = path.extname(bundleActualPath).toLowerCase().replace('.', ''); // Wrap in XML with proper escaping const wrappedContent = this.wrapContentInXml(fileContent, bundleFilePath, fileExt); dependencies.set(bundleFilePath, wrappedContent); } } } return { skipped: false }; } /** * Include core workflow task files */ async includeCoreWorkflowFiles(dependencies, processed, moduleName, warnings = []) { const coreWorkflowPath = 'bmad/core/tasks/workflow.md'; if (processed.has(coreWorkflowPath)) { return; } processed.add(coreWorkflowPath); const actualPath = this.resolveFilePath(coreWorkflowPath, moduleName); if (!actualPath || !(await fs.pathExists(actualPath))) { warnings.push(coreWorkflowPath); return; } const fileContent = await fs.readFile(actualPath, 'utf8'); const wrappedContent = this.wrapContentInXml(fileContent, coreWorkflowPath, 'md'); dependencies.set(coreWorkflowPath, wrappedContent); } /** * Include advanced elicitation files */ async includeAdvancedElicitationFiles(dependencies, processed, moduleName, warnings = []) { const elicitationFiles = ['bmad/core/tasks/adv-elicit.md', 'bmad/core/tasks/adv-elicit-methods.csv']; for (const filePath of elicitationFiles) { if (processed.has(filePath)) { continue; } processed.add(filePath); const actualPath = this.resolveFilePath(filePath, moduleName); if (!actualPath || !(await fs.pathExists(actualPath))) { warnings.push(filePath); continue; } const fileContent = await fs.readFile(actualPath, 'utf8'); const fileExt = path.extname(actualPath).toLowerCase().replace('.', ''); const wrappedContent = this.wrapContentInXml(fileContent, filePath, fileExt); dependencies.set(filePath, wrappedContent); } } /** * Wrap file content in XML with proper escaping */ wrapContentInXml(content, id, type = 'text') { // Escape any ]]> sequences in the content by splitting CDATA sections // Replace ]]> with ]]]]> to properly escape it within CDATA const escapedContent = content.replaceAll(']]>', ']]]]>'); // Use CDATA to preserve content exactly as-is, including special characters return ``; } /** * Process wildcard dependency patterns */ async processWildcardDependency(pattern, dependencies, processed, moduleName, warnings = []) { // Remove {project-root} prefix pattern = pattern.replace(/^{project-root}\//, ''); // Get directory and file pattern const lastSlash = pattern.lastIndexOf('/'); const dirPath = pattern.slice(0, Math.max(0, lastSlash)); const filePattern = pattern.slice(Math.max(0, lastSlash + 1)); // Resolve directory path without checking file existence let dir; if (dirPath.startsWith('bmad/')) { // Remove bmad/ prefix const actualPath = dirPath.replace(/^bmad\//, ''); // Try different path mappings for directories const possibleDirs = [ // Try as module path: bmad/cis/... -> src/modules/cis/... path.join(this.sourceDir, 'modules', actualPath), // Try as direct path: bmad/core/... -> src/core/... path.join(this.sourceDir, actualPath), ]; for (const testDir of possibleDirs) { if (fs.existsSync(testDir)) { dir = testDir; break; } } } if (!dir) { warnings.push(`${pattern} (could not resolve directory)`); return; } if (!(await fs.pathExists(dir))) { warnings.push(pattern); return; } // Read directory and match files const files = await fs.readdir(dir); let matchedFiles = []; if (filePattern === '*.*') { matchedFiles = files; } else if (filePattern.startsWith('*.')) { const ext = filePattern.slice(1); matchedFiles = files.filter((f) => f.endsWith(ext)); } else { // Simple glob matching const regex = new RegExp('^' + filePattern.replace('*', '.*') + '$'); matchedFiles = files.filter((f) => regex.test(f)); } // Process each matched file for (const file of matchedFiles) { const fullPath = dirPath + '/' + file; if (!processed.has(fullPath)) { await this.processFileDependency(fullPath, dependencies, processed, moduleName, warnings); } } } /** * Resolve file path relative to project */ resolveFilePath(filePath, moduleName) { // Remove {project-root} prefix filePath = filePath.replace(/^{project-root}\//, ''); // Check temp directory first for _cfg files if (filePath.startsWith('bmad/_cfg/')) { const filename = filePath.split('/').pop(); const tempPath = path.join(this.tempManifestDir, filename); if (fs.existsSync(tempPath)) { return tempPath; } } // Handle different path patterns for bmad files // bmad/cis/tasks/brain-session.md -> src/modules/cis/tasks/brain-session.md // bmad/core/tasks/create-doc.md -> src/core/tasks/create-doc.md // bmad/bmm/templates/brief.md -> src/modules/bmm/templates/brief.md let actualPath = filePath; if (filePath.startsWith('bmad/')) { // Remove bmad/ prefix actualPath = filePath.replace(/^bmad\//, ''); // Check if it's a module-specific file (cis, bmm, etc) or core file const parts = actualPath.split('/'); const firstPart = parts[0]; // Try different path mappings const possiblePaths = [ // Try in temp directory first path.join(this.tempDir, filePath), // Try as module path: bmad/cis/... -> src/modules/cis/... path.join(this.sourceDir, 'modules', actualPath), // Try as direct path: bmad/core/... -> src/core/... path.join(this.sourceDir, actualPath), // Try without any prefix in src path.join(this.sourceDir, parts.slice(1).join('/')), // Try in project root path.join(this.sourceDir, '..', actualPath), // Try original with bmad path.join(this.sourceDir, '..', filePath), ]; for (const testPath of possiblePaths) { if (fs.existsSync(testPath)) { return testPath; } } } // Try standard paths for non-bmad files const basePaths = [ this.sourceDir, // src directory path.join(this.modulesPath, moduleName), // Current module path.join(this.sourceDir, '..'), // Project root ]; for (const basePath of basePaths) { const fullPath = path.join(basePath, actualPath); if (fs.existsSync(fullPath)) { return fullPath; } } return null; } /** * Process and remove {project-root} references */ processProjectRootReferences(content) { // Remove {project-root}/ prefix (with slash) content = content.replaceAll('{project-root}/', ''); // Also remove {project-root} without slash content = content.replaceAll('{project-root}', ''); return content; } /** * Escape special XML characters in text content */ escapeXmlText(text) { return text .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } /** * Escape XML content while preserving XML tags */ escapeXmlContent(content) { const tagPattern = /<([^>]+)>/g; const parts = []; let lastIndex = 0; let match; while ((match = tagPattern.exec(content)) !== null) { if (match.index > lastIndex) { parts.push(this.escapeXmlText(content.slice(lastIndex, match.index))); } parts.push('<' + match[1] + '>'); lastIndex = match.index + match[0].length; } if (lastIndex < content.length) { parts.push(this.escapeXmlText(content.slice(lastIndex))); } return parts.join(''); } /** * Inject web activation instructions into agent XML */ injectWebActivation(agentXml) { // Check if agent already has an activation block if (agentXml.includes('[\s\S]*?<\/critical-actions>/, activationXml); return injectedXml; } // If no critical-actions, inject before closing tag const closingTagMatch = agentXml.match(/(\s*)<\/agent>/); if (!closingTagMatch) { console.warn(chalk.yellow('Warning: Could not find tag for activation injection')); return agentXml; } // Inject the activation block before the closing tag // Properly indent each line of the activation XML const indent = closingTagMatch[1]; const indentedActivation = activationXml .split('\n') .map((line) => (line.trim() ? indent + line : '')) .join('\n'); const injectedXml = agentXml.replace(/(\s*)<\/agent>/, `\n${indentedActivation}\n${indent}`); return injectedXml; } /** * Build the final agent bundle XML */ buildAgentBundle(agentXml, dependencies) { // Inject web activation instructions into agent XML agentXml = this.injectWebActivation(agentXml); const parts = [ '', '', ' ', ' ' + agentXml.replaceAll('\n', '\n '), ]; // Add dependencies without wrapper tags if (dependencies && dependencies.size > 0) { parts.push('\n '); for (const [id, content] of dependencies) { // Check if content is already wrapped in a tag (from workflow processing) // If so, don't escape it - it's already in CDATA const isWrappedFile = content.trim().startsWith(''); const escapedContent = isWrappedFile ? content : this.escapeXmlContent(content); // Indent properly const indentedContent = escapedContent .split('\n') .map((line) => ' ' + line) .join('\n'); parts.push(indentedContent); } } parts.push(''); return parts.join('\n'); } /** * Discover all modules */ async discoverModules() { const modules = []; if (!(await fs.pathExists(this.modulesPath))) { console.log(chalk.yellow('No modules directory found')); return modules; } const entries = await fs.readdir(this.modulesPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { modules.push(entry.name); } } return modules; } /** * Discover agents in a module */ async discoverAgents(modulePath) { const agents = []; const agentsPath = path.join(modulePath, 'agents'); if (!(await fs.pathExists(agentsPath))) { return agents; } const files = await fs.readdir(agentsPath); for (const file of files) { if (file.endsWith('.md') && !file.toLowerCase().includes('readme')) { agents.push(file); } } return agents; } /** * Discover all teams in a module */ async discoverTeams(modulePath) { const teams = []; const teamsPath = path.join(modulePath, 'teams'); if (!(await fs.pathExists(teamsPath))) { return teams; } const files = await fs.readdir(teamsPath); for (const file of files) { if (file.endsWith('.md')) { teams.push(file); } } return teams; } /** * Extract agent name from XML */ getAgentName(xml) { const match = xml.match(/]*name="([^"]+)"/); return match ? match[1] : 'Unknown'; } /** * Extract agent description from XML */ getAgentDescription(xml) { const match = xml.match(/([^<]+)<\/description>/); return match ? match[1] : ''; } /** * Check if agent should be skipped for bundling */ shouldSkipBundling(xml) { // Check for bundle="false" attribute in the agent tag const match = xml.match(/]*bundle="false"[^>]*>/); return match !== null; } /** * Create temporary manifest files */ async createTempManifests() { // Ensure temp directory exists await fs.ensureDir(this.tempManifestDir); // Generate agent-party.xml using shared generator const agentPartyPath = path.join(this.tempManifestDir, 'agent-party.xml'); await AgentPartyGenerator.writeAgentParty(agentPartyPath, this.discoveredAgents, { forWeb: true }); console.log(chalk.dim(' ✓ Created temporary manifest files')); } /** * Clean up temporary files */ async cleanupTempFiles() { if (await fs.pathExists(this.tempDir)) { await fs.remove(this.tempDir); console.log(chalk.dim('\n✓ Cleaned up temporary files')); } } /** * Validate XML content */ async validateXml(xmlContent) { try { await xml2js.parseStringPromise(xmlContent, { strict: true, explicitArray: false, }); return true; } catch { return false; } } /** * Display summary statistics */ displaySummary() { console.log(chalk.cyan.bold('\n═══════════════════════════════════════════════')); console.log(chalk.cyan.bold(' SUMMARY')); console.log(chalk.cyan.bold('═══════════════════════════════════════════════\n')); console.log(chalk.bold('Bundle Statistics:')); console.log(` Total agents found: ${this.stats.totalAgents}`); console.log(` Successfully bundled: ${chalk.green(this.stats.bundledAgents)}`); console.log(` Skipped (bundle=false): ${chalk.gray(this.stats.skippedAgents)}`); if (this.stats.failedAgents > 0) { console.log(` Failed to bundle: ${chalk.red(this.stats.failedAgents)}`); } if (this.stats.invalidXml > 0) { console.log(` Invalid XML bundles: ${chalk.yellow(this.stats.invalidXml)}`); } // Display warnings summary if (this.stats.warnings.length > 0) { console.log(chalk.yellow('\n⚠ Missing Dependencies by Agent:')); // Group and display warnings by agent for (const agentWarning of this.stats.warnings) { if (agentWarning.warnings.length > 0) { console.log(chalk.bold(`\n ${agentWarning.agent}:`)); // Display unique warnings for this agent const uniqueWarnings = [...new Set(agentWarning.warnings)]; for (const warning of uniqueWarnings) { console.log(chalk.dim(` • ${warning}`)); } } } } // Final status if (this.stats.invalidXml > 0) { console.log(chalk.yellow('\n⚠ Some bundles have invalid XML. Please review the output.')); } else if (this.stats.failedAgents > 0) { console.log(chalk.yellow('\n⚠ Some agents failed to bundle. Please review the errors.')); } else { console.log(chalk.green('\n✨ All bundles generated successfully!')); } console.log(chalk.cyan.bold('\n═══════════════════════════════════════════════\n')); } } module.exports = { WebBundler };