doc updates, build folder renamed to tools, readme clarity for v4
This commit is contained in:
262
tools/lib/dependency-resolver.js
Normal file
262
tools/lib/dependency-resolver.js
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user