/** * BMAD v4 Dependency Resolver * Analyzes agent configurations and resolves resource dependencies */ const fs = require('fs'); const path = require('path'); const yaml = require('js-yaml'); class DependencyResolver { constructor(rootPath = process.cwd()) { this.rootPath = rootPath; this.agentsPath = path.join(rootPath, 'agents'); this.corePath = path.join(rootPath, 'bmad-core'); this.cache = new Map(); } /** * Load and parse an agent configuration */ loadAgentConfig(agentId) { if (this.cache.has(agentId)) { return this.cache.get(agentId); } const configPath = path.join(this.agentsPath, `${agentId}.yml`); if (!fs.existsSync(configPath)) { throw new Error(`Agent configuration not found: ${configPath}`); } const configContent = fs.readFileSync(configPath, 'utf8'); const rawConfig = yaml.load(configContent); // Extract agent config from nested structure if present const config = rawConfig.agent || rawConfig; // Merge other root-level fields that might be needed if (rawConfig.dependencies) { config.dependencies = rawConfig.dependencies; } // Validate required fields this.validateAgentConfig(config, agentId); this.cache.set(agentId, config); return config; } /** * Validate agent configuration structure */ validateAgentConfig(config, agentId) { const required = ['name', 'id']; for (const field of required) { if (!config[field]) { throw new Error(`Missing required field '${field}' in agent ${agentId}`); } } if (config.id !== agentId) { throw new Error(`Agent ID mismatch: expected '${agentId}', got '${config.id}'`); } // Ensure persona exists if (!config.persona) { // Default to agent id if no persona specified config.persona = config.id; } // Ensure dependencies exist with defaults if (!config.dependencies) { config.dependencies = { tasks: [], templates: [], checklists: [], data: [] }; } } /** * Resolve dependencies for a single agent */ resolveAgentDependencies(agentId, environment = 'web') { const config = this.loadAgentConfig(agentId); const dependencies = { agent: agentId, config: config, resources: { tasks: config.dependencies?.tasks || [], templates: config.dependencies?.templates || [], checklists: config.dependencies?.checklists || [], data: config.dependencies?.data || [], utils: config.dependencies?.utils || [] } }; // Validate that all required resources exist this.validateResourceExistence(dependencies.resources); return dependencies; } /** * Resolve dependencies for multiple agents (bundle) */ resolveBundleDependencies(agentIds, environment = 'web', optimize = true) { const agentDependencies = []; const allResources = { tasks: new Set(), templates: new Set(), checklists: new Set(), data: new Set(), utils: new Set() }; // Collect dependencies for each agent for (const agentId of agentIds) { const deps = this.resolveAgentDependencies(agentId, environment); agentDependencies.push(deps); // Aggregate all resources Object.keys(allResources).forEach(type => { deps.resources[type].forEach(resource => { allResources[type].add(resource); }); }); } const result = { agents: agentDependencies, bundleResources: { tasks: Array.from(allResources.tasks), templates: Array.from(allResources.templates), checklists: Array.from(allResources.checklists), data: Array.from(allResources.data), utils: Array.from(allResources.utils) }, optimized: optimize }; if (optimize) { result.statistics = this.calculateOptimizationStats(agentDependencies, result.bundleResources); } return result; } /** * Calculate optimization statistics */ calculateOptimizationStats(agentDeps, bundleResources) { const totalAgents = agentDeps.length; const totalResources = Object.values(bundleResources).reduce((sum, arr) => sum + arr.length, 0); // Calculate how many resources would be needed without optimization const unoptimizedTotal = agentDeps.reduce((sum, agent) => { return sum + Object.values(agent.resources).reduce((agentSum, arr) => agentSum + arr.length, 0); }, 0); const savings = unoptimizedTotal - totalResources; const savingsPercentage = unoptimizedTotal > 0 ? (savings / unoptimizedTotal * 100).toFixed(1) : 0; return { totalAgents, totalUniqueResources: totalResources, unoptimizedResourceCount: unoptimizedTotal, resourcesSaved: savings, optimizationPercentage: savingsPercentage }; } /** * Validate that all required resources exist in core */ validateResourceExistence(resources) { const resourceTypes = ['tasks', 'templates', 'checklists', 'data', 'utils']; for (const type of resourceTypes) { const resourceDir = path.join(this.corePath, type); for (const resource of resources[type] || []) { const resourcePath = path.join(resourceDir, `${resource}.md`); const altPath = path.join(resourceDir, `${resource}.yml`); if (!fs.existsSync(resourcePath) && !fs.existsSync(altPath)) { throw new Error(`Resource not found: ${type}/${resource} (checked .md and .yml)`); } } } } /** * Get all available agents */ getAvailableAgents() { if (!fs.existsSync(this.agentsPath)) { return []; } return fs.readdirSync(this.agentsPath) .filter(file => { return (file.endsWith('.yml') || file.endsWith('.yaml')) && fs.statSync(path.join(this.agentsPath, file)).isFile(); }) .map(file => path.basename(file, path.extname(file))); } /** * Generate dependency graph for visualization */ generateDependencyGraph(agentIds = null) { const agents = agentIds || this.getAvailableAgents(); const graph = { nodes: [], edges: [] }; for (const agentId of agents) { const config = this.loadAgentConfig(agentId); // Add agent node graph.nodes.push({ id: agentId, type: 'agent', label: config.name, description: config.description }); // Add resource nodes and edges Object.entries(config.requires).forEach(([type, resources]) => { resources.forEach(resource => { const resourceId = `${type}:${resource}`; // Add resource node if not exists if (!graph.nodes.find(n => n.id === resourceId)) { graph.nodes.push({ id: resourceId, type: type, label: resource, category: type }); } // Add edge graph.edges.push({ from: agentId, to: resourceId, type: 'requires' }); }); }); } return graph; } } module.exports = DependencyResolver;