build tools temporarily removed, replacement incoming
This commit is contained in:
@@ -1,546 +0,0 @@
|
|||||||
/**
|
|
||||||
* BMAD v4 Web Builder
|
|
||||||
* Builds optimized web bundles and standalone agents
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const DependencyResolver = require('../lib/dependency-resolver');
|
|
||||||
const BundleOptimizer = require('../lib/bundle-optimizer');
|
|
||||||
|
|
||||||
class WebBuilder {
|
|
||||||
constructor(rootPath = process.cwd()) {
|
|
||||||
this.rootPath = rootPath;
|
|
||||||
this.agentsPath = path.join(rootPath, 'bmad-core', 'agents');
|
|
||||||
this.teamsPath = path.join(rootPath, 'bmad-core', 'agent-teams');
|
|
||||||
this.outputPath = path.join(rootPath, 'dist');
|
|
||||||
this.sampleUpdatePath = path.join(rootPath, 'web-bundles');
|
|
||||||
this.resolver = new DependencyResolver(rootPath);
|
|
||||||
this.optimizer = new BundleOptimizer(rootPath);
|
|
||||||
this.sampleUpdateEnabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable sample update mode to output to web-bundles directory as well
|
|
||||||
*/
|
|
||||||
enableSampleUpdate() {
|
|
||||||
this.sampleUpdateEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build all web bundles
|
|
||||||
*/
|
|
||||||
async buildAll() {
|
|
||||||
console.log('🚀 Building all bundles...');
|
|
||||||
|
|
||||||
const results = {
|
|
||||||
teams: [],
|
|
||||||
bundles: [],
|
|
||||||
agents: [],
|
|
||||||
errors: []
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Ensure output directories exist
|
|
||||||
this.ensureOutputDirectory();
|
|
||||||
if (this.sampleUpdateEnabled) {
|
|
||||||
this.ensureSampleUpdateDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build orchestrator bundles
|
|
||||||
const bundleConfigs = this.loadBundleConfigs();
|
|
||||||
for (const config of bundleConfigs) {
|
|
||||||
try {
|
|
||||||
const result = await this.buildBundle(config);
|
|
||||||
const isTeamBundle = config.name.toLowerCase().includes('team');
|
|
||||||
if (isTeamBundle) {
|
|
||||||
results.teams.push(result);
|
|
||||||
} else {
|
|
||||||
results.bundles.push(result);
|
|
||||||
}
|
|
||||||
const bundleType = isTeamBundle ? 'team bundle' : 'bundle';
|
|
||||||
console.log(`✅ Built ${bundleType}: ${config.name}`);
|
|
||||||
} catch (error) {
|
|
||||||
const isTeamBundle = config.name.toLowerCase().includes('team');
|
|
||||||
const bundleType = isTeamBundle ? 'team bundle' : 'bundle';
|
|
||||||
const errorType = isTeamBundle ? 'team' : 'bundle';
|
|
||||||
console.error(`❌ Failed to build ${bundleType} ${config.name}:`, error.message);
|
|
||||||
results.errors.push({ type: errorType, name: config.name, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build all individual agents as standalone bundles
|
|
||||||
const availableAgents = this.resolver.getAvailableAgents();
|
|
||||||
|
|
||||||
// Filter out team bundles and include all non-team agents
|
|
||||||
const individualAgents = availableAgents.filter(agentId => {
|
|
||||||
// Skip team bundles
|
|
||||||
if (agentId.startsWith('team-')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const config = this.resolver.loadAgentConfig(agentId);
|
|
||||||
// Build all agents that don't explicitly disable web
|
|
||||||
return config.environments?.web?.available !== false;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const agentId of individualAgents) {
|
|
||||||
try {
|
|
||||||
const result = await this.buildStandaloneAgent(agentId);
|
|
||||||
results.agents.push(result);
|
|
||||||
console.log(`✅ Built agent bundle: ${agentId}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Failed to build agent ${agentId}:`, error.message);
|
|
||||||
results.errors.push({ type: 'agent', name: agentId, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n📊 Build Summary:`);
|
|
||||||
console.log(` Teams: ${results.teams.length} built, ${results.errors.filter(e => e.type === 'team').length} failed`);
|
|
||||||
if (results.bundles.length > 0 || results.errors.filter(e => e.type === 'bundle').length > 0) {
|
|
||||||
console.log(` Bundles: ${results.bundles.length} built, ${results.errors.filter(e => e.type === 'bundle').length} failed`);
|
|
||||||
}
|
|
||||||
console.log(` Agents: ${results.agents.length} built, ${results.errors.filter(e => e.type === 'agent').length} failed`);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Build failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expand agent wildcards in the list
|
|
||||||
* If the list contains '*', it will be replaced with all available agents
|
|
||||||
*/
|
|
||||||
expandAgentWildcards(agentIds) {
|
|
||||||
// Check if wildcard is present
|
|
||||||
const wildcardIndex = agentIds.indexOf('*');
|
|
||||||
if (wildcardIndex === -1) {
|
|
||||||
return agentIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all available agents
|
|
||||||
const allAgents = this.resolver.getAvailableAgents()
|
|
||||||
.filter(agentId => {
|
|
||||||
// Exclude team bundles
|
|
||||||
if (agentId.startsWith('team-')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const config = this.resolver.loadAgentConfig(agentId);
|
|
||||||
// Include all agents that don't explicitly disable web
|
|
||||||
return config.environments?.web?.available !== false;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create expanded list
|
|
||||||
const expandedList = [...agentIds];
|
|
||||||
|
|
||||||
// Replace wildcard with all agents not already in the list
|
|
||||||
const existingAgents = new Set(agentIds.filter(id => id !== '*'));
|
|
||||||
const agentsToAdd = allAgents.filter(agent => !existingAgents.has(agent));
|
|
||||||
|
|
||||||
// Replace the wildcard with the missing agents
|
|
||||||
expandedList.splice(wildcardIndex, 1, ...agentsToAdd);
|
|
||||||
|
|
||||||
console.log(` Expanded wildcard to include: ${agentsToAdd.join(', ')}`);
|
|
||||||
|
|
||||||
return expandedList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a specific bundle
|
|
||||||
*/
|
|
||||||
async buildBundle(bundleConfig) {
|
|
||||||
const isTeamBundle = bundleConfig.name.toLowerCase().includes('team');
|
|
||||||
const emoji = isTeamBundle ? '👥' : '📦';
|
|
||||||
const bundleType = isTeamBundle ? 'team bundle' : 'bundle';
|
|
||||||
console.log(`${emoji} Building ${bundleType}: ${bundleConfig.name}`);
|
|
||||||
|
|
||||||
// Ensure agents is an array of strings
|
|
||||||
let agentIds = Array.isArray(bundleConfig.agents) ? bundleConfig.agents : [];
|
|
||||||
|
|
||||||
// Expand wildcards
|
|
||||||
agentIds = this.expandAgentWildcards(agentIds);
|
|
||||||
|
|
||||||
// Resolve dependencies
|
|
||||||
const agentDependencies = this.resolver.resolveBundleDependencies(
|
|
||||||
agentIds,
|
|
||||||
'web',
|
|
||||||
bundleConfig.optimize !== false
|
|
||||||
);
|
|
||||||
|
|
||||||
// Optimize bundle
|
|
||||||
const optimizedBundle = this.optimizer.optimizeBundle(bundleConfig, agentDependencies);
|
|
||||||
|
|
||||||
// Validate bundle
|
|
||||||
const validation = this.optimizer.validateBundle(optimizedBundle, {
|
|
||||||
maxBundleSize: bundleConfig.max_bundle_size
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!validation.valid) {
|
|
||||||
throw new Error(`Bundle validation failed: ${validation.issues.map(i => i.message).join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write output files
|
|
||||||
const outputDir = path.join(this.outputPath, 'teams');
|
|
||||||
this.ensureDirectory(outputDir);
|
|
||||||
|
|
||||||
const outputs = [];
|
|
||||||
|
|
||||||
// Default to single_file format if not specified
|
|
||||||
const outputFormat = bundleConfig.output?.format || 'single_file';
|
|
||||||
const outputFilename = bundleConfig.output?.filename || bundleConfig.filename || `${bundleConfig.name.toLowerCase().replace(/\s+/g, '-')}.txt`;
|
|
||||||
|
|
||||||
if (outputFormat === 'single_file') {
|
|
||||||
// Create single bundle file
|
|
||||||
const bundleContent = this.createBundleContent(optimizedBundle, bundleConfig);
|
|
||||||
const bundleFile = path.join(outputDir, outputFilename);
|
|
||||||
|
|
||||||
fs.writeFileSync(bundleFile, bundleContent);
|
|
||||||
outputs.push(bundleFile);
|
|
||||||
|
|
||||||
// Also write to web-bundles if sample update is enabled
|
|
||||||
if (this.sampleUpdateEnabled) {
|
|
||||||
const sampleOutputDir = path.join(this.sampleUpdatePath, 'teams');
|
|
||||||
this.ensureDirectory(sampleOutputDir);
|
|
||||||
const sampleBundleFile = path.join(sampleOutputDir, outputFilename);
|
|
||||||
fs.writeFileSync(sampleBundleFile, bundleContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For single_file format, everything is in the bundle file
|
|
||||||
// No need for separate orchestrator files
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: bundleConfig.name,
|
|
||||||
type: 'bundle',
|
|
||||||
outputs: outputs,
|
|
||||||
statistics: optimizedBundle.statistics,
|
|
||||||
validation: validation
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a standalone agent
|
|
||||||
*/
|
|
||||||
async buildStandaloneAgent(agentId) {
|
|
||||||
console.log(`👤 Building standalone agent: ${agentId}`);
|
|
||||||
|
|
||||||
const optimizedBundle = this.optimizer.createStandaloneAgent(agentId, 'web');
|
|
||||||
|
|
||||||
// Get agent config to extract name
|
|
||||||
const agentConfig = this.resolver.loadAgentConfig(agentId);
|
|
||||||
const agentName = agentConfig.name || agentId;
|
|
||||||
|
|
||||||
// Create lowercase-dashcase filename with format: {id}-{name}.txt
|
|
||||||
const filename = `${agentId}-${agentName.toLowerCase().replace(/\s+/g, '-')}.txt`;
|
|
||||||
|
|
||||||
// Write standalone agent file
|
|
||||||
const outputDir = path.join(this.outputPath, 'agents');
|
|
||||||
this.ensureDirectory(outputDir);
|
|
||||||
|
|
||||||
const agentFile = path.join(outputDir, filename);
|
|
||||||
fs.writeFileSync(agentFile, optimizedBundle.standaloneContent);
|
|
||||||
|
|
||||||
// Also write to web-bundles if sample update is enabled
|
|
||||||
if (this.sampleUpdateEnabled) {
|
|
||||||
const sampleOutputDir = path.join(this.sampleUpdatePath, 'agents');
|
|
||||||
this.ensureDirectory(sampleOutputDir);
|
|
||||||
const sampleAgentFile = path.join(sampleOutputDir, filename);
|
|
||||||
fs.writeFileSync(sampleAgentFile, optimizedBundle.standaloneContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: agentId,
|
|
||||||
type: 'standalone_agent',
|
|
||||||
outputs: [agentFile],
|
|
||||||
statistics: optimizedBundle.statistics
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create bundle content for single file output
|
|
||||||
*/
|
|
||||||
createBundleContent(bundle, config) {
|
|
||||||
// For a truly self-contained bundle, start with the orchestrator prompt
|
|
||||||
let content = this.createOrchestratorPrompt(bundle, config);
|
|
||||||
|
|
||||||
content += '\n\n';
|
|
||||||
|
|
||||||
// Add agent configurations section
|
|
||||||
content += `==================== START: agent-config ====================\n`;
|
|
||||||
const configData = {
|
|
||||||
name: bundle.metadata.name,
|
|
||||||
version: bundle.metadata.version || '1.0.0',
|
|
||||||
agents: bundle.agents,
|
|
||||||
commands: config.output?.orchestrator_commands || []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Include workflows if defined
|
|
||||||
if (config.workflows) {
|
|
||||||
configData.workflows = config.workflows;
|
|
||||||
}
|
|
||||||
|
|
||||||
content += yaml.dump(configData);
|
|
||||||
content += `==================== END: agent-config ====================\n\n`;
|
|
||||||
|
|
||||||
// Add resource sections
|
|
||||||
bundle.sections.forEach(section => {
|
|
||||||
content += section.content + '\n\n';
|
|
||||||
});
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create orchestrator files (agent-prompt.txt and agent-config.txt)
|
|
||||||
*/
|
|
||||||
createOrchestratorFiles(bundle, config) {
|
|
||||||
const files = [];
|
|
||||||
const outputDir = path.join(this.outputPath, 'teams');
|
|
||||||
|
|
||||||
// Create agent-config.txt
|
|
||||||
const agentConfigContent = yaml.dump({
|
|
||||||
name: bundle.metadata.name,
|
|
||||||
version: bundle.metadata.version || '1.0.0',
|
|
||||||
environment: 'web',
|
|
||||||
agents: bundle.agents,
|
|
||||||
commands: config.output?.orchestrator_commands || [],
|
|
||||||
metadata: {
|
|
||||||
generatedAt: bundle.metadata.generatedAt,
|
|
||||||
totalResources: bundle.statistics.totalResources,
|
|
||||||
optimization: bundle.metadata.optimization
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
files.push({
|
|
||||||
path: path.join(outputDir, 'agent-config.txt'),
|
|
||||||
content: agentConfigContent
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create agent-prompt.txt (orchestrator instructions)
|
|
||||||
const promptContent = this.createOrchestratorPrompt(bundle, config);
|
|
||||||
files.push({
|
|
||||||
path: path.join(outputDir, 'agent-prompt.txt'),
|
|
||||||
content: promptContent
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create individual section files
|
|
||||||
bundle.sections.forEach(section => {
|
|
||||||
files.push({
|
|
||||||
path: path.join(outputDir, section.filename),
|
|
||||||
content: section.content
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create orchestrator prompt content
|
|
||||||
*/
|
|
||||||
createOrchestratorPrompt(bundle, config) {
|
|
||||||
// Try to use the bmad persona as the orchestrator base
|
|
||||||
const bmadPersonaPath = path.join(this.rootPath, 'bmad-core', 'personas', 'bmad.md');
|
|
||||||
|
|
||||||
if (fs.existsSync(bmadPersonaPath)) {
|
|
||||||
const bmadContent = fs.readFileSync(bmadPersonaPath, 'utf8');
|
|
||||||
// Append bundle-specific agent information
|
|
||||||
let prompt = bmadContent + '\n\n';
|
|
||||||
prompt += `## Available Agents in ${bundle.metadata.name}\n\n`;
|
|
||||||
|
|
||||||
Object.entries(bundle.agents).forEach(([id, agent]) => {
|
|
||||||
const command = config.output?.orchestrator_commands?.find(cmd => cmd.includes(id)) || `/${id}`;
|
|
||||||
prompt += `### ${agent.name} (${command})\n`;
|
|
||||||
prompt += `- **Role:** ${agent.title}\n`;
|
|
||||||
prompt += `- **Description:** ${agent.description}\n`;
|
|
||||||
if (agent.customize) {
|
|
||||||
prompt += `- **Customization:** ${agent.customize}\n`;
|
|
||||||
}
|
|
||||||
prompt += '\n';
|
|
||||||
});
|
|
||||||
|
|
||||||
return prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to basic prompt if bmad persona not found
|
|
||||||
|
|
||||||
let prompt = `# BMAD ${bundle.metadata.name} Orchestrator\n\n`;
|
|
||||||
prompt += `You are the BMAD orchestrator for the ${bundle.metadata.name}. `;
|
|
||||||
prompt += `You can transform into any of the following specialized agents:\n\n`;
|
|
||||||
|
|
||||||
// List available agents
|
|
||||||
Object.entries(bundle.agents).forEach(([id, agent]) => {
|
|
||||||
prompt += `## ${agent.name} (${config.output?.orchestrator_commands?.find(cmd => cmd.includes(id)) || `/${id}`})\n`;
|
|
||||||
prompt += `**Role:** ${agent.title}\n`;
|
|
||||||
prompt += `${agent.description}\n`;
|
|
||||||
if (agent.customize) {
|
|
||||||
prompt += `**Customization:** ${agent.customize}\n`;
|
|
||||||
}
|
|
||||||
prompt += '\n';
|
|
||||||
if (agent.capabilities && agent.capabilities.length > 0) {
|
|
||||||
prompt += `**Capabilities:**\n`;
|
|
||||||
agent.capabilities.forEach(cap => prompt += `- ${cap}\n`);
|
|
||||||
prompt += '\n';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
prompt += `## Usage\n\n`;
|
|
||||||
prompt += `To transform into a specific agent, use the corresponding command:\n`;
|
|
||||||
(config.output?.orchestrator_commands || []).forEach(cmd => {
|
|
||||||
prompt += `- \`${cmd}\` - Transform into the corresponding agent\n`;
|
|
||||||
});
|
|
||||||
|
|
||||||
prompt += `\n## Resources Available\n\n`;
|
|
||||||
prompt += `This bundle includes ${bundle.statistics.totalResources} resources:\n`;
|
|
||||||
Object.entries(bundle.statistics.resourcesByType).forEach(([type, count]) => {
|
|
||||||
prompt += `- ${count} ${type}\n`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load all bundle configurations
|
|
||||||
*/
|
|
||||||
loadBundleConfigs() {
|
|
||||||
const configs = [];
|
|
||||||
|
|
||||||
// Load team configurations from agent-teams directory
|
|
||||||
const teamFiles = this.findAgentFiles(this.teamsPath);
|
|
||||||
teamFiles.forEach(file => {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(file, 'utf8');
|
|
||||||
const config = yaml.load(content);
|
|
||||||
|
|
||||||
// Check if this has bundle config
|
|
||||||
if (config.bundle) {
|
|
||||||
// Merge agents list from root level into bundle config
|
|
||||||
const bundleConfig = { ...config.bundle };
|
|
||||||
if (config.agents && !bundleConfig.agents) {
|
|
||||||
bundleConfig.agents = config.agents;
|
|
||||||
}
|
|
||||||
// Include workflows if defined
|
|
||||||
if (config.workflows) {
|
|
||||||
bundleConfig.workflows = config.workflows;
|
|
||||||
}
|
|
||||||
configs.push(bundleConfig);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Warning: Failed to load config ${file}:`, error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// For backward compatibility, also check agents directory for team-*.yml files
|
|
||||||
const agentFiles = this.findAgentFiles(this.agentsPath);
|
|
||||||
agentFiles.forEach(file => {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(file, 'utf8');
|
|
||||||
const config = yaml.load(content);
|
|
||||||
const filename = path.basename(file);
|
|
||||||
|
|
||||||
// Check if this is a team bundle (team-*.yml) with bundle config
|
|
||||||
if (filename.startsWith('team-') && config.bundle) {
|
|
||||||
// Merge agents list from root level into bundle config
|
|
||||||
const bundleConfig = { ...config.bundle };
|
|
||||||
if (config.agents && !bundleConfig.agents) {
|
|
||||||
bundleConfig.agents = config.agents;
|
|
||||||
}
|
|
||||||
configs.push(bundleConfig);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Warning: Failed to load config ${file}:`, error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return configs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all agent configuration files
|
|
||||||
*/
|
|
||||||
findAgentFiles(dir) {
|
|
||||||
const files = [];
|
|
||||||
|
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = fs.readdirSync(dir);
|
|
||||||
items.forEach(item => {
|
|
||||||
const itemPath = path.join(dir, item);
|
|
||||||
const stat = fs.statSync(itemPath);
|
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
files.push(...this.findAgentFiles(itemPath));
|
|
||||||
} else if (item.endsWith('.yml') || item.endsWith('.yaml')) {
|
|
||||||
files.push(itemPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure directory exists
|
|
||||||
*/
|
|
||||||
ensureDirectory(dir) {
|
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure output directory exists
|
|
||||||
*/
|
|
||||||
ensureOutputDirectory() {
|
|
||||||
this.ensureDirectory(this.outputPath);
|
|
||||||
this.ensureDirectory(path.join(this.outputPath, 'teams'));
|
|
||||||
this.ensureDirectory(path.join(this.outputPath, 'agents'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure sample update directory exists
|
|
||||||
*/
|
|
||||||
ensureSampleUpdateDirectory() {
|
|
||||||
// Clean existing files in web-bundles subdirectories
|
|
||||||
const teamsDir = path.join(this.sampleUpdatePath, 'teams');
|
|
||||||
const agentsDir = path.join(this.sampleUpdatePath, 'agents');
|
|
||||||
|
|
||||||
// Remove existing .txt files in teams directory
|
|
||||||
if (fs.existsSync(teamsDir)) {
|
|
||||||
const teamFiles = fs.readdirSync(teamsDir).filter(f => f.endsWith('.txt'));
|
|
||||||
teamFiles.forEach(file => {
|
|
||||||
fs.unlinkSync(path.join(teamsDir, file));
|
|
||||||
});
|
|
||||||
console.log(`🧹 Cleaned ${teamFiles.length} files from web-bundles/teams`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove existing .txt files in agents directory
|
|
||||||
if (fs.existsSync(agentsDir)) {
|
|
||||||
const agentFiles = fs.readdirSync(agentsDir).filter(f => f.endsWith('.txt'));
|
|
||||||
agentFiles.forEach(file => {
|
|
||||||
fs.unlinkSync(path.join(agentsDir, file));
|
|
||||||
});
|
|
||||||
console.log(`🧹 Cleaned ${agentFiles.length} files from web-bundles/agents`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure directories exist
|
|
||||||
this.ensureDirectory(this.sampleUpdatePath);
|
|
||||||
this.ensureDirectory(teamsDir);
|
|
||||||
this.ensureDirectory(agentsDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = WebBuilder;
|
|
||||||
215
tools/cli.js
215
tools/cli.js
@@ -1,215 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BMAD v4 Build CLI
|
|
||||||
* Command line interface for building agents and bundles
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { program } = require('commander');
|
|
||||||
const WebBuilder = require('./builders/web-builder');
|
|
||||||
const DependencyResolver = require('./lib/dependency-resolver');
|
|
||||||
|
|
||||||
// Initialize resolver
|
|
||||||
const resolver = new DependencyResolver();
|
|
||||||
|
|
||||||
program
|
|
||||||
.name('bmad-build')
|
|
||||||
.description('BMAD v4 Build System')
|
|
||||||
.version('4.0.0');
|
|
||||||
|
|
||||||
// Build all web bundles and agents
|
|
||||||
program
|
|
||||||
.command('build')
|
|
||||||
.alias('build:web')
|
|
||||||
.description('Build all web bundles and standalone agents')
|
|
||||||
.option('--sample-update', 'Also output to web-bundles directory')
|
|
||||||
.action(async (options) => {
|
|
||||||
try {
|
|
||||||
const builder = new WebBuilder();
|
|
||||||
if (options.sampleUpdate) {
|
|
||||||
builder.enableSampleUpdate();
|
|
||||||
}
|
|
||||||
const results = await builder.buildAll();
|
|
||||||
|
|
||||||
if (results.errors.length > 0) {
|
|
||||||
console.log('\n⚠️ Build completed with errors:');
|
|
||||||
results.errors.forEach(error => {
|
|
||||||
console.log(` ${error.type}: ${error.name} - ${error.error}`);
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
console.log('\n🎉 All builds completed successfully!');
|
|
||||||
if (options.sampleUpdate) {
|
|
||||||
console.log(' 📁 Also updated web-bundles directory');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Build failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build specific bundle
|
|
||||||
program
|
|
||||||
.command('build:bundle')
|
|
||||||
.description('Build a specific bundle')
|
|
||||||
.requiredOption('-n, --name <name>', 'Bundle name')
|
|
||||||
.action(async (options) => {
|
|
||||||
try {
|
|
||||||
const builder = new WebBuilder();
|
|
||||||
const configs = builder.loadBundleConfigs();
|
|
||||||
const config = configs.find(c => c.name.toLowerCase().includes(options.name.toLowerCase()));
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
console.error(`❌ Bundle not found: ${options.name}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await builder.buildBundle(config);
|
|
||||||
console.log(`✅ Built bundle: ${result.name}`);
|
|
||||||
console.log(` Files: ${result.outputs.length}`);
|
|
||||||
console.log(` Size: ${result.statistics.totalSize} characters`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Build failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build standalone agent
|
|
||||||
program
|
|
||||||
.command('build:agent')
|
|
||||||
.description('Build a standalone agent')
|
|
||||||
.requiredOption('-a, --agent <id>', 'Agent ID')
|
|
||||||
.action(async (options) => {
|
|
||||||
try {
|
|
||||||
const builder = new WebBuilder();
|
|
||||||
const result = await builder.buildStandaloneAgent(options.agent);
|
|
||||||
|
|
||||||
console.log(`✅ Built agent: ${result.name}`);
|
|
||||||
console.log(` File: ${result.outputs[0]}`);
|
|
||||||
console.log(` Size: ${result.statistics.totalSize} characters`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Build failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// List available agents
|
|
||||||
program
|
|
||||||
.command('list:agents')
|
|
||||||
.description('List all available agents')
|
|
||||||
.action(() => {
|
|
||||||
try {
|
|
||||||
const agents = resolver.getAvailableAgents();
|
|
||||||
console.log('📋 Available agents:');
|
|
||||||
|
|
||||||
agents.forEach(agentId => {
|
|
||||||
try {
|
|
||||||
const config = resolver.loadAgentConfig(agentId);
|
|
||||||
const webCompatible = config.environments?.web?.available !== false;
|
|
||||||
const ideOnly = config.environments?.ide?.ide_only === true;
|
|
||||||
|
|
||||||
console.log(` ${agentId}: ${config.name}`);
|
|
||||||
console.log(` ${config.description}`);
|
|
||||||
console.log(` Environments: ${webCompatible ? 'web' : ''}${webCompatible && !ideOnly ? ', ' : ''}${!ideOnly ? 'ide' : 'ide-only'}`);
|
|
||||||
console.log('');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ${agentId}: Error loading config`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to list agents:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Analyze dependencies
|
|
||||||
program
|
|
||||||
.command('analyze:deps')
|
|
||||||
.description('Analyze agent dependencies')
|
|
||||||
.option('-a, --agent <id>', 'Specific agent ID')
|
|
||||||
.option('-g, --graph', 'Generate dependency graph')
|
|
||||||
.action((options) => {
|
|
||||||
try {
|
|
||||||
if (options.agent) {
|
|
||||||
const deps = resolver.resolveAgentDependencies(options.agent, 'web');
|
|
||||||
console.log(`📊 Dependencies for ${deps.config.name}:`);
|
|
||||||
Object.entries(deps.resources).forEach(([type, resources]) => {
|
|
||||||
if (resources.length > 0) {
|
|
||||||
console.log(` ${type}: ${resources.join(', ')}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (options.graph) {
|
|
||||||
const graph = resolver.generateDependencyGraph();
|
|
||||||
console.log('📈 Dependency Graph:');
|
|
||||||
console.log(` Nodes: ${graph.nodes.length}`);
|
|
||||||
console.log(` Edges: ${graph.edges.length}`);
|
|
||||||
console.log('\n Agents:');
|
|
||||||
graph.nodes.filter(n => n.type === 'agent').forEach(n => {
|
|
||||||
console.log(` ${n.id}: ${n.label}`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const agents = resolver.getAvailableAgents();
|
|
||||||
console.log('📊 All Agent Dependencies:');
|
|
||||||
agents.forEach(agentId => {
|
|
||||||
try {
|
|
||||||
const deps = resolver.resolveAgentDependencies(agentId, 'web');
|
|
||||||
const totalDeps = Object.values(deps.resources).reduce((sum, arr) => sum + arr.length, 0);
|
|
||||||
console.log(` ${agentId}: ${totalDeps} total dependencies`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ${agentId}: Error resolving dependencies`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Analysis failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate configuration
|
|
||||||
program
|
|
||||||
.command('validate')
|
|
||||||
.description('Validate all configurations')
|
|
||||||
.action(() => {
|
|
||||||
try {
|
|
||||||
const agents = resolver.getAvailableAgents();
|
|
||||||
let errors = 0;
|
|
||||||
|
|
||||||
console.log('🔍 Validating configurations...');
|
|
||||||
|
|
||||||
agents.forEach(agentId => {
|
|
||||||
try {
|
|
||||||
const deps = resolver.resolveAgentDependencies(agentId, 'web');
|
|
||||||
console.log(` ✅ ${agentId}: Valid`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ ${agentId}: ${error.message}`);
|
|
||||||
errors++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errors > 0) {
|
|
||||||
console.log(`\n❌ Validation failed: ${errors} errors found`);
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
console.log('\n✅ All configurations valid!');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Validation failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle unknown commands
|
|
||||||
program.on('command:*', () => {
|
|
||||||
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' '));
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse command line arguments
|
|
||||||
program.parse();
|
|
||||||
|
|
||||||
// Show help if no command provided
|
|
||||||
if (!process.argv.slice(2).length) {
|
|
||||||
program.outputHelp();
|
|
||||||
}
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
|
|
||||||
// Colors for console output
|
|
||||||
const colors = {
|
|
||||||
reset: '\x1b[0m',
|
|
||||||
bright: '\x1b[1m',
|
|
||||||
green: '\x1b[32m',
|
|
||||||
red: '\x1b[31m',
|
|
||||||
yellow: '\x1b[33m',
|
|
||||||
blue: '\x1b[34m',
|
|
||||||
cyan: '\x1b[36m'
|
|
||||||
};
|
|
||||||
|
|
||||||
function log(message, color = 'reset') {
|
|
||||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function error(message) {
|
|
||||||
console.error(`${colors.red}❌ Error: ${message}${colors.reset}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function success(message) {
|
|
||||||
console.log(`${colors.green}✅ ${message}${colors.reset}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function info(message) {
|
|
||||||
console.log(`${colors.blue}ℹ️ ${message}${colors.reset}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installExpansionPack(packName) {
|
|
||||||
const expansionPackPath = path.join(__dirname, '..', 'expansion-packs', packName);
|
|
||||||
const manifestPath = path.join(expansionPackPath, 'manifest.yml');
|
|
||||||
|
|
||||||
// Check if expansion pack exists
|
|
||||||
if (!fs.existsSync(expansionPackPath)) {
|
|
||||||
error(`Expansion pack '${packName}' not found`);
|
|
||||||
log('\nAvailable expansion packs:', 'cyan');
|
|
||||||
const packsDir = path.join(__dirname, '..', 'expansion-packs');
|
|
||||||
const packs = fs.readdirSync(packsDir)
|
|
||||||
.filter(f => fs.statSync(path.join(packsDir, f)).isDirectory())
|
|
||||||
.filter(f => f !== 'README.md');
|
|
||||||
packs.forEach(pack => log(` - ${pack}`, 'cyan'));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load manifest
|
|
||||||
if (!fs.existsSync(manifestPath)) {
|
|
||||||
error(`Manifest file not found for expansion pack '${packName}'`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let manifest;
|
|
||||||
try {
|
|
||||||
const manifestContent = fs.readFileSync(manifestPath, 'utf8');
|
|
||||||
manifest = yaml.load(manifestContent);
|
|
||||||
} catch (e) {
|
|
||||||
error(`Failed to parse manifest: ${e.message}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`\n${colors.bright}Installing ${manifest.name} v${manifest.version}${colors.reset}`, 'bright');
|
|
||||||
log(`${manifest.description}\n`, 'cyan');
|
|
||||||
|
|
||||||
// Create directories if needed
|
|
||||||
const projectRoot = path.join(__dirname, '..');
|
|
||||||
const bmadCore = path.join(projectRoot, 'bmad-core');
|
|
||||||
|
|
||||||
// Install files
|
|
||||||
let installedCount = 0;
|
|
||||||
let skippedCount = 0;
|
|
||||||
|
|
||||||
for (const fileMapping of manifest.files) {
|
|
||||||
const sourcePath = path.join(expansionPackPath, fileMapping.source);
|
|
||||||
const destPath = path.join(projectRoot, fileMapping.destination);
|
|
||||||
const destDir = path.dirname(destPath);
|
|
||||||
|
|
||||||
// Create destination directory if it doesn't exist
|
|
||||||
if (!fs.existsSync(destDir)) {
|
|
||||||
fs.mkdirSync(destDir, { recursive: true });
|
|
||||||
info(`Created directory: ${path.relative(projectRoot, destDir)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file already exists
|
|
||||||
if (fs.existsSync(destPath)) {
|
|
||||||
const response = await promptUser(`File ${path.relative(projectRoot, destPath)} already exists. Overwrite? (y/N): `);
|
|
||||||
if (response.toLowerCase() !== 'y') {
|
|
||||||
info(`Skipped: ${fileMapping.source}`);
|
|
||||||
skippedCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy file
|
|
||||||
try {
|
|
||||||
fs.copyFileSync(sourcePath, destPath);
|
|
||||||
success(`Installed: ${fileMapping.source} → ${path.relative(projectRoot, destPath)}`);
|
|
||||||
installedCount++;
|
|
||||||
} catch (e) {
|
|
||||||
error(`Failed to install ${fileMapping.source}: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update team configurations
|
|
||||||
if (manifest.team_updates && manifest.team_updates.length > 0) {
|
|
||||||
log('\nUpdating team configurations...', 'yellow');
|
|
||||||
|
|
||||||
for (const update of manifest.team_updates) {
|
|
||||||
// Try new location first (agent-teams), then fallback to old location (agents)
|
|
||||||
let teamPath = path.join(projectRoot, 'agent-teams', update.team);
|
|
||||||
if (!fs.existsSync(teamPath)) {
|
|
||||||
teamPath = path.join(projectRoot, 'agents', update.team);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.existsSync(teamPath)) {
|
|
||||||
try {
|
|
||||||
let teamConfig = yaml.load(fs.readFileSync(teamPath, 'utf8'));
|
|
||||||
|
|
||||||
if (!teamConfig.agents) {
|
|
||||||
teamConfig.agents = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!teamConfig.agents.includes(update.add_agent)) {
|
|
||||||
teamConfig.agents.push(update.add_agent);
|
|
||||||
fs.writeFileSync(teamPath, yaml.dump(teamConfig));
|
|
||||||
success(`Updated ${update.team} with ${update.add_agent} agent`);
|
|
||||||
} else {
|
|
||||||
info(`${update.team} already includes ${update.add_agent} agent`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
error(`Failed to update ${update.team}: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show summary
|
|
||||||
log(`\n${colors.bright}Installation Summary${colors.reset}`, 'bright');
|
|
||||||
log(`Files installed: ${installedCount}`, 'green');
|
|
||||||
if (skippedCount > 0) {
|
|
||||||
log(`Files skipped: ${skippedCount}`, 'yellow');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show post-install message
|
|
||||||
if (manifest.post_install_message) {
|
|
||||||
log(`\n${colors.bright}Next Steps:${colors.reset}`, 'bright');
|
|
||||||
log(manifest.post_install_message, 'cyan');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remind to rebuild
|
|
||||||
log('\nRemember to rebuild bundles:', 'yellow');
|
|
||||||
log(' npm run build', 'yellow');
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptUser(question) {
|
|
||||||
const readline = require('readline');
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
rl.question(question, answer => {
|
|
||||||
rl.close();
|
|
||||||
resolve(answer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main execution
|
|
||||||
const packName = process.argv[2];
|
|
||||||
|
|
||||||
if (!packName) {
|
|
||||||
log(`${colors.bright}BMAD Method Expansion Pack Installer${colors.reset}\n`, 'bright');
|
|
||||||
log('Usage: node install-expansion-pack.js <pack-name>', 'yellow');
|
|
||||||
log('\nExample:', 'cyan');
|
|
||||||
log(' node install-expansion-pack.js infrastructure', 'cyan');
|
|
||||||
|
|
||||||
log('\nAvailable expansion packs:', 'cyan');
|
|
||||||
const packsDir = path.join(__dirname, '..', 'expansion-packs');
|
|
||||||
if (fs.existsSync(packsDir)) {
|
|
||||||
const packs = fs.readdirSync(packsDir)
|
|
||||||
.filter(f => fs.statSync(path.join(packsDir, f)).isDirectory())
|
|
||||||
.filter(f => f !== 'README.md');
|
|
||||||
packs.forEach(pack => {
|
|
||||||
const manifestPath = path.join(packsDir, pack, 'manifest.yml');
|
|
||||||
if (fs.existsSync(manifestPath)) {
|
|
||||||
try {
|
|
||||||
const manifest = yaml.load(fs.readFileSync(manifestPath, 'utf8'));
|
|
||||||
log(` - ${pack}: ${manifest.description}`, 'cyan');
|
|
||||||
} catch (e) {
|
|
||||||
log(` - ${pack}`, 'cyan');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
installExpansionPack(packName).catch(err => {
|
|
||||||
error(`Installation failed: ${err.message}`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
/**
|
|
||||||
* BMAD v4 Bundle Optimizer
|
|
||||||
* Optimizes bundles by deduplicating resources and minimizing size
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
class BundleOptimizer {
|
|
||||||
constructor(rootPath = process.cwd()) {
|
|
||||||
this.rootPath = rootPath;
|
|
||||||
this.corePath = path.join(rootPath, 'bmad-core');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optimize a bundle by loading and processing resources
|
|
||||||
*/
|
|
||||||
optimizeBundle(bundleConfig, agentDependencies) {
|
|
||||||
const optimizedBundle = {
|
|
||||||
metadata: {
|
|
||||||
name: bundleConfig.name,
|
|
||||||
version: bundleConfig.version,
|
|
||||||
environment: 'web',
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
optimization: bundleConfig.optimize || false
|
|
||||||
},
|
|
||||||
agents: {},
|
|
||||||
resources: {
|
|
||||||
personas: {},
|
|
||||||
tasks: {},
|
|
||||||
templates: {},
|
|
||||||
checklists: {},
|
|
||||||
data: {},
|
|
||||||
utils: {}
|
|
||||||
},
|
|
||||||
sections: [],
|
|
||||||
statistics: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process each agent
|
|
||||||
agentDependencies.agents.forEach(agentDep => {
|
|
||||||
optimizedBundle.agents[agentDep.agent] = {
|
|
||||||
name: agentDep.config.name,
|
|
||||||
id: agentDep.config.id,
|
|
||||||
title: agentDep.config.title,
|
|
||||||
description: agentDep.config.description,
|
|
||||||
persona: agentDep.config.persona,
|
|
||||||
customize: agentDep.config.customize || '',
|
|
||||||
capabilities: agentDep.config.capabilities || [],
|
|
||||||
workflow: agentDep.config.workflow || []
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load and process resources
|
|
||||||
this.loadResources(optimizedBundle, agentDependencies.bundleResources, agentDependencies.agents);
|
|
||||||
|
|
||||||
// Create optimized sections for web output
|
|
||||||
this.createWebSections(optimizedBundle, bundleConfig);
|
|
||||||
|
|
||||||
// Calculate statistics
|
|
||||||
optimizedBundle.statistics = this.calculateBundleStats(optimizedBundle, agentDependencies);
|
|
||||||
|
|
||||||
return optimizedBundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load resources from core directory
|
|
||||||
*/
|
|
||||||
loadResources(bundle, resourceLists, agentDeps = []) {
|
|
||||||
const resourceTypes = ['tasks', 'templates', 'checklists', 'data', 'utils'];
|
|
||||||
|
|
||||||
resourceTypes.forEach(type => {
|
|
||||||
const resourceDir = path.join(this.corePath, type);
|
|
||||||
|
|
||||||
(resourceLists[type] || []).forEach(resourceName => {
|
|
||||||
const content = this.loadResourceFile(resourceDir, resourceName);
|
|
||||||
if (content) {
|
|
||||||
bundle.resources[type][resourceName] = {
|
|
||||||
name: resourceName,
|
|
||||||
content: content,
|
|
||||||
size: content.length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load personas for agents
|
|
||||||
const personaDir = path.join(this.corePath, 'personas');
|
|
||||||
agentDeps.forEach(agentDep => {
|
|
||||||
const agentId = agentDep.agent;
|
|
||||||
const personaName = agentDep.config.persona || agentId;
|
|
||||||
const personaContent = this.loadResourceFile(personaDir, personaName);
|
|
||||||
if (personaContent) {
|
|
||||||
bundle.resources.personas[agentId] = {
|
|
||||||
name: personaName,
|
|
||||||
content: personaContent,
|
|
||||||
size: personaContent.length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a resource file from disk
|
|
||||||
*/
|
|
||||||
loadResourceFile(dir, name) {
|
|
||||||
const extensions = ['.md', '.yml', '.yaml'];
|
|
||||||
|
|
||||||
for (const ext of extensions) {
|
|
||||||
const filePath = path.join(dir, `${name}${ext}`);
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
return fs.readFileSync(filePath, 'utf8');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn(`Resource file not found: ${name} in ${dir}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create web-formatted sections with markers
|
|
||||||
*/
|
|
||||||
createWebSections(bundle, bundleConfig) {
|
|
||||||
const sections = [];
|
|
||||||
|
|
||||||
// Create personas section
|
|
||||||
// For team bundles, exclude BMAD from personas since it's already the orchestrator
|
|
||||||
if (Object.keys(bundle.resources.personas).length > 0) {
|
|
||||||
const personasContent = Object.entries(bundle.resources.personas)
|
|
||||||
.filter(([id, persona]) => id !== 'bmad') // Exclude BMAD from personas section
|
|
||||||
.map(([id, persona]) =>
|
|
||||||
`==================== START: personas#${id} ====================\n` +
|
|
||||||
persona.content +
|
|
||||||
`\n==================== END: personas#${id} ====================`
|
|
||||||
).join('\n\n');
|
|
||||||
|
|
||||||
if (personasContent) { // Only add section if there's content after filtering
|
|
||||||
sections.push({
|
|
||||||
name: 'personas',
|
|
||||||
filename: 'personas.txt',
|
|
||||||
content: personasContent,
|
|
||||||
size: personasContent.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create other resource sections
|
|
||||||
['tasks', 'templates', 'checklists', 'data', 'utils'].forEach(type => {
|
|
||||||
const resources = bundle.resources[type];
|
|
||||||
if (Object.keys(resources).length > 0) {
|
|
||||||
const sectionContent = Object.entries(resources)
|
|
||||||
.map(([name, resource]) =>
|
|
||||||
`==================== START: ${type}#${name} ====================\n` +
|
|
||||||
resource.content +
|
|
||||||
`\n==================== END: ${type}#${name} ====================`
|
|
||||||
).join('\n\n');
|
|
||||||
|
|
||||||
sections.push({
|
|
||||||
name: type,
|
|
||||||
filename: `${type}.txt`,
|
|
||||||
content: sectionContent,
|
|
||||||
size: sectionContent.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
bundle.sections = sections;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create standalone agent bundle
|
|
||||||
*/
|
|
||||||
createStandaloneAgent(agentId, environment = 'web') {
|
|
||||||
const DependencyResolver = require('./dependency-resolver');
|
|
||||||
const resolver = new DependencyResolver(this.rootPath);
|
|
||||||
|
|
||||||
const agentDep = resolver.resolveAgentDependencies(agentId, environment);
|
|
||||||
const bundleConfig = {
|
|
||||||
name: `${agentDep.config.name} Standalone`,
|
|
||||||
version: agentDep.config.version || '1.0.0',
|
|
||||||
// Environment is always 'web' for standalone agents
|
|
||||||
optimize: true
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create bundle with just this agent
|
|
||||||
const agentDependencies = {
|
|
||||||
agents: [agentDep],
|
|
||||||
bundleResources: agentDep.resources
|
|
||||||
};
|
|
||||||
|
|
||||||
const optimizedBundle = this.optimizeBundle(bundleConfig, agentDependencies);
|
|
||||||
|
|
||||||
// For standalone agents, create a single combined content
|
|
||||||
if (environment === 'web') {
|
|
||||||
optimizedBundle.standaloneContent = this.createStandaloneContent(optimizedBundle, agentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return optimizedBundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create single-file content for standalone agent
|
|
||||||
*/
|
|
||||||
createStandaloneContent(bundle, agentId) {
|
|
||||||
const agent = bundle.agents[agentId];
|
|
||||||
const persona = bundle.resources.personas[agentId];
|
|
||||||
|
|
||||||
let content = `# ${agent.name}\n\n`;
|
|
||||||
content += `${agent.description}\n\n`;
|
|
||||||
|
|
||||||
if (persona) {
|
|
||||||
content += `==================== START: personas#${agentId} ====================\n`;
|
|
||||||
content += `${persona.content}\n`;
|
|
||||||
content += `==================== END: personas#${agentId} ====================\n\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add required resources inline
|
|
||||||
const resourceTypes = ['tasks', 'templates', 'checklists', 'data', 'utils'];
|
|
||||||
resourceTypes.forEach(type => {
|
|
||||||
const resources = bundle.resources[type];
|
|
||||||
if (Object.keys(resources).length > 0) {
|
|
||||||
Object.entries(resources).forEach(([name, resource]) => {
|
|
||||||
content += `==================== START: ${type}#${name} ====================\n`;
|
|
||||||
content += `${resource.content}\n`;
|
|
||||||
content += `==================== END: ${type}#${name} ====================\n\n`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate bundle statistics
|
|
||||||
*/
|
|
||||||
calculateBundleStats(bundle, agentDependencies) {
|
|
||||||
const stats = {
|
|
||||||
agents: Object.keys(bundle.agents).length,
|
|
||||||
totalResources: 0,
|
|
||||||
resourcesByType: {},
|
|
||||||
totalSize: 0,
|
|
||||||
sizeByType: {},
|
|
||||||
averageResourceSize: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Count resources and calculate sizes
|
|
||||||
Object.entries(bundle.resources).forEach(([type, resources]) => {
|
|
||||||
const count = Object.keys(resources).length;
|
|
||||||
stats.resourcesByType[type] = count;
|
|
||||||
stats.totalResources += count;
|
|
||||||
|
|
||||||
const typeSize = Object.values(resources).reduce((sum, r) => sum + (r.size || 0), 0);
|
|
||||||
stats.sizeByType[type] = typeSize;
|
|
||||||
stats.totalSize += typeSize;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (stats.totalResources > 0) {
|
|
||||||
stats.averageResourceSize = Math.round(stats.totalSize / stats.totalResources);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add web-specific stats
|
|
||||||
if (bundle.sections) {
|
|
||||||
stats.webSections = bundle.sections.length;
|
|
||||||
stats.webTotalSize = bundle.sections.reduce((sum, s) => sum + s.size, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate bundle against size constraints
|
|
||||||
*/
|
|
||||||
validateBundle(bundle, constraints = {}) {
|
|
||||||
const issues = [];
|
|
||||||
const stats = bundle.statistics;
|
|
||||||
|
|
||||||
// Check max bundle size
|
|
||||||
if (constraints.maxBundleSize && stats.totalSize > constraints.maxBundleSize) {
|
|
||||||
issues.push({
|
|
||||||
type: 'size_exceeded',
|
|
||||||
message: `Bundle size ${stats.totalSize} exceeds limit ${constraints.maxBundleSize}`,
|
|
||||||
severity: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check web section sizes
|
|
||||||
if (bundle.sections) {
|
|
||||||
bundle.sections.forEach(section => {
|
|
||||||
if (constraints.maxSectionSize && section.size > constraints.maxSectionSize) {
|
|
||||||
issues.push({
|
|
||||||
type: 'section_size_exceeded',
|
|
||||||
message: `Section ${section.name} size ${section.size} exceeds limit ${constraints.maxSectionSize}`,
|
|
||||||
severity: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: issues.filter(i => i.severity === 'error').length === 0,
|
|
||||||
issues: issues
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = BundleOptimizer;
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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, 'bmad-core', '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;
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
/**
|
|
||||||
* BMAD v5 Unified Dependency Resolver
|
|
||||||
* Works with unified agent configurations that can generate both IDE and web outputs
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
|
|
||||||
class UnifiedDependencyResolver {
|
|
||||||
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 a unified agent configuration
|
|
||||||
*/
|
|
||||||
loadUnifiedAgentConfig(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(`Unified agent configuration not found: ${configPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(configPath, 'utf8');
|
|
||||||
const config = yaml.load(content);
|
|
||||||
|
|
||||||
// Validate unified config structure
|
|
||||||
this.validateUnifiedConfig(config, agentId);
|
|
||||||
|
|
||||||
this.cache.set(agentId, config);
|
|
||||||
return config;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to load unified agent config ${agentId}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate unified configuration structure
|
|
||||||
*/
|
|
||||||
validateUnifiedConfig(config, agentId) {
|
|
||||||
if (!config.agent || !config.agent.id || !config.agent.name) {
|
|
||||||
throw new Error(`Invalid unified config for ${agentId}: missing agent identity`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.dependencies) {
|
|
||||||
throw new Error(`Invalid unified config for ${agentId}: missing dependencies`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.environments || !config.environments.web || !config.environments.ide) {
|
|
||||||
throw new Error(`Invalid unified config for ${agentId}: missing environment configurations`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRITICAL: Ensure ONLY BMAD has bmad-kb access
|
|
||||||
const hasBmadKb = (
|
|
||||||
config.dependencies.data?.includes('bmad-kb') ||
|
|
||||||
config.environments.ide.dependencies?.data?.includes('bmad-kb') ||
|
|
||||||
config.environments.web.dependencies?.data?.includes('bmad-kb')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasBmadKb && agentId !== 'bmad') {
|
|
||||||
throw new Error(`SECURITY VIOLATION: Agent ${agentId} has bmad-kb access but only BMAD should have it!`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasBmadKb && agentId === 'bmad') {
|
|
||||||
throw new Error(`Configuration error: BMAD agent missing required bmad-kb access`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve dependencies for an agent in a specific environment
|
|
||||||
*/
|
|
||||||
resolveAgentDependencies(agentId, environment = 'web', bundleContext = null) {
|
|
||||||
const config = this.loadUnifiedAgentConfig(agentId);
|
|
||||||
|
|
||||||
// Start with base dependencies
|
|
||||||
const baseDeps = {
|
|
||||||
tasks: [...(config.dependencies.tasks || [])],
|
|
||||||
templates: [...(config.dependencies.templates || [])],
|
|
||||||
checklists: [...(config.dependencies.checklists || [])],
|
|
||||||
data: [...(config.dependencies.data || [])]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply environment-specific overrides
|
|
||||||
const envConfig = config.environments[environment];
|
|
||||||
if (envConfig && envConfig.dependencies) {
|
|
||||||
Object.keys(envConfig.dependencies).forEach(type => {
|
|
||||||
if (envConfig.dependencies[type]) {
|
|
||||||
baseDeps[type] = [...new Set([...baseDeps[type], ...envConfig.dependencies[type]])];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special handling for team bundles containing BMAD
|
|
||||||
if (bundleContext && bundleContext.agents && bundleContext.agents.includes('bmad')) {
|
|
||||||
// Only add bmad-kb if this IS the BMAD agent
|
|
||||||
if (agentId === 'bmad') {
|
|
||||||
if (!baseDeps.data.includes('bmad-kb')) {
|
|
||||||
baseDeps.data.push('bmad-kb');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve file paths and validate existence
|
|
||||||
const resolvedDeps = {};
|
|
||||||
Object.keys(baseDeps).forEach(type => {
|
|
||||||
resolvedDeps[type] = baseDeps[type].map(dep => {
|
|
||||||
const filePath = this.resolveResourcePath(type, dep);
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
throw new Error(`Resource not found: ${filePath} (required by ${agentId})`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: dep,
|
|
||||||
path: filePath,
|
|
||||||
content: fs.readFileSync(filePath, 'utf8')
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
config: config,
|
|
||||||
agentId: agentId,
|
|
||||||
environment: environment,
|
|
||||||
resources: resolvedDeps
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve path for a resource
|
|
||||||
*/
|
|
||||||
resolveResourcePath(type, resourceId) {
|
|
||||||
const resourceMap = {
|
|
||||||
'tasks': 'tasks',
|
|
||||||
'templates': 'templates',
|
|
||||||
'checklists': 'checklists',
|
|
||||||
'data': 'data',
|
|
||||||
'personas': 'personas',
|
|
||||||
'utils': 'utils'
|
|
||||||
};
|
|
||||||
|
|
||||||
const subdir = resourceMap[type];
|
|
||||||
if (!subdir) {
|
|
||||||
throw new Error(`Unknown resource type: ${type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.join(this.corePath, subdir, `${resourceId}.md`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all available agents
|
|
||||||
*/
|
|
||||||
getAvailableAgents() {
|
|
||||||
if (!fs.existsSync(this.agentsPath)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs.readdirSync(this.agentsPath)
|
|
||||||
.filter(file => file.endsWith('.yml'))
|
|
||||||
.map(file => path.basename(file, '.yml'))
|
|
||||||
.sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate dependency graph for all agents
|
|
||||||
*/
|
|
||||||
generateDependencyGraph() {
|
|
||||||
const agents = this.getAvailableAgents();
|
|
||||||
const nodes = [];
|
|
||||||
const edges = [];
|
|
||||||
|
|
||||||
// Add agent nodes
|
|
||||||
agents.forEach(agentId => {
|
|
||||||
try {
|
|
||||||
const config = this.loadUnifiedAgentConfig(agentId);
|
|
||||||
nodes.push({
|
|
||||||
id: agentId,
|
|
||||||
type: 'agent',
|
|
||||||
label: config.agent.name,
|
|
||||||
title: config.agent.title,
|
|
||||||
hasBmadKb: config.dependencies.data?.includes('bmad-kb') || false
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Skipping ${agentId}: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add resource nodes and edges
|
|
||||||
agents.forEach(agentId => {
|
|
||||||
try {
|
|
||||||
const deps = this.resolveAgentDependencies(agentId, 'web');
|
|
||||||
Object.entries(deps.resources).forEach(([type, resources]) => {
|
|
||||||
resources.forEach(resource => {
|
|
||||||
const resourceNodeId = `${type}:${resource.id}`;
|
|
||||||
|
|
||||||
// Add resource node if not exists
|
|
||||||
if (!nodes.find(n => n.id === resourceNodeId)) {
|
|
||||||
nodes.push({
|
|
||||||
id: resourceNodeId,
|
|
||||||
type: 'resource',
|
|
||||||
resourceType: type,
|
|
||||||
label: resource.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add edge from agent to resource
|
|
||||||
edges.push({
|
|
||||||
from: agentId,
|
|
||||||
to: resourceNodeId,
|
|
||||||
type: 'requires'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to analyze dependencies for ${agentId}: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { nodes, edges };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate all configurations
|
|
||||||
*/
|
|
||||||
validateAllConfigurations() {
|
|
||||||
const agents = this.getAvailableAgents();
|
|
||||||
const results = {
|
|
||||||
valid: [],
|
|
||||||
invalid: [],
|
|
||||||
bmadKbViolations: []
|
|
||||||
};
|
|
||||||
|
|
||||||
agents.forEach(agentId => {
|
|
||||||
try {
|
|
||||||
const deps = this.resolveAgentDependencies(agentId, 'web');
|
|
||||||
results.valid.push(agentId);
|
|
||||||
|
|
||||||
// Check for bmad-kb violations
|
|
||||||
const hasBmadKb = deps.resources.data?.some(d => d.id === 'bmad-kb');
|
|
||||||
if (hasBmadKb && agentId !== 'bmad') {
|
|
||||||
results.bmadKbViolations.push(agentId);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
results.invalid.push({
|
|
||||||
agentId: agentId,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = UnifiedDependencyResolver;
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BMAD v5 Unified Build CLI
|
|
||||||
* Works with unified agent configurations
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { program } = require('commander');
|
|
||||||
const UnifiedWebBuilder = require('./builders/unified-web-builder');
|
|
||||||
const UnifiedDependencyResolver = require('./lib/unified-dependency-resolver');
|
|
||||||
|
|
||||||
// Initialize resolver
|
|
||||||
const resolver = new UnifiedDependencyResolver();
|
|
||||||
|
|
||||||
program
|
|
||||||
.name('bmad-unified-build')
|
|
||||||
.description('BMAD v5 Unified Build System')
|
|
||||||
.version('5.0.0');
|
|
||||||
|
|
||||||
// Build all web outputs
|
|
||||||
program
|
|
||||||
.command('build:web')
|
|
||||||
.description('Build all web outputs from unified configurations')
|
|
||||||
.action(async () => {
|
|
||||||
try {
|
|
||||||
const builder = new UnifiedWebBuilder();
|
|
||||||
const results = await builder.buildAll();
|
|
||||||
|
|
||||||
if (results.errors.length > 0) {
|
|
||||||
console.log('\\n⚠️ Build completed with errors:');
|
|
||||||
results.errors.forEach(error => {
|
|
||||||
console.log(` ${error.type}: ${error.name} - ${error.error}`);
|
|
||||||
});
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
console.log('\\n🎉 All builds completed successfully!');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Build failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build specific agent
|
|
||||||
program
|
|
||||||
.command('build:agent')
|
|
||||||
.description('Build a specific standalone agent')
|
|
||||||
.requiredOption('-a, --agent <id>', 'Agent ID')
|
|
||||||
.action(async (options) => {
|
|
||||||
try {
|
|
||||||
const builder = new UnifiedWebBuilder();
|
|
||||||
const result = await builder.buildStandaloneAgent(options.agent);
|
|
||||||
|
|
||||||
console.log(`✅ Built agent: ${result.name}`);
|
|
||||||
console.log(` File: ${result.outputs[0]}`);
|
|
||||||
console.log(` Size: ${result.size} characters`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Build failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// List available agents
|
|
||||||
program
|
|
||||||
.command('list:agents')
|
|
||||||
.description('List all available agents from unified configs')
|
|
||||||
.action(() => {
|
|
||||||
try {
|
|
||||||
const agents = resolver.getAvailableAgents();
|
|
||||||
console.log('📋 Available agents:');
|
|
||||||
|
|
||||||
agents.forEach(agentId => {
|
|
||||||
try {
|
|
||||||
const config = resolver.loadUnifiedAgentConfig(agentId);
|
|
||||||
const webCompatible = config.environments.web?.available !== false;
|
|
||||||
const ideOnly = config.environments.ide?.ide_only === true;
|
|
||||||
const hasBmadKb = config.dependencies.data?.includes('bmad-kb');
|
|
||||||
|
|
||||||
console.log(` ${agentId}: ${config.agent.name} (${config.agent.title})`);
|
|
||||||
console.log(` ${config.agent.description}`);
|
|
||||||
console.log(` Environments: ${webCompatible ? 'web' : ''}${webCompatible && !ideOnly ? ', ' : ''}${!ideOnly ? 'ide' : 'ide-only'}`);
|
|
||||||
if (hasBmadKb) {
|
|
||||||
console.log(` 🔑 Has BMAD-KB access`);
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ${agentId}: Error loading config - ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to list agents:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate configurations
|
|
||||||
program
|
|
||||||
.command('validate')
|
|
||||||
.description('Validate all unified configurations')
|
|
||||||
.action(() => {
|
|
||||||
try {
|
|
||||||
console.log('🔍 Validating unified configurations...');
|
|
||||||
|
|
||||||
const results = resolver.validateAllConfigurations();
|
|
||||||
|
|
||||||
console.log(`\\n📊 Validation Results:`);
|
|
||||||
console.log(` Valid: ${results.valid.length}`);
|
|
||||||
console.log(` Invalid: ${results.invalid.length}`);
|
|
||||||
console.log(` BMAD-KB Violations: ${results.bmadKbViolations.length}`);
|
|
||||||
|
|
||||||
if (results.invalid.length > 0) {
|
|
||||||
console.log('\\n❌ Invalid Configurations:');
|
|
||||||
results.invalid.forEach(item => {
|
|
||||||
console.log(` ${item.agentId}: ${item.error}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.bmadKbViolations.length > 0) {
|
|
||||||
console.log('\\n🚨 CRITICAL: BMAD-KB Security Violations:');
|
|
||||||
results.bmadKbViolations.forEach(agentId => {
|
|
||||||
console.log(` ${agentId}: Has bmad-kb access but only BMAD should have it!`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.invalid.length > 0 || results.bmadKbViolations.length > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
console.log('\\n✅ All configurations valid and secure!');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Validation failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Analyze dependencies
|
|
||||||
program
|
|
||||||
.command('analyze:deps')
|
|
||||||
.description('Analyze agent dependencies')
|
|
||||||
.option('-a, --agent <id>', 'Specific agent ID')
|
|
||||||
.option('-g, --graph', 'Generate dependency graph')
|
|
||||||
.action((options) => {
|
|
||||||
try {
|
|
||||||
if (options.agent) {
|
|
||||||
const deps = resolver.resolveAgentDependencies(options.agent, 'web');
|
|
||||||
console.log(`📊 Dependencies for ${deps.config.agent.name} (${deps.agentId}):`);
|
|
||||||
Object.entries(deps.resources).forEach(([type, resources]) => {
|
|
||||||
if (resources.length > 0) {
|
|
||||||
console.log(` ${type}: ${resources.map(r => r.id).join(', ')}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for BMAD-KB access
|
|
||||||
const hasBmadKb = deps.resources.data?.some(r => r.id === 'bmad-kb');
|
|
||||||
if (hasBmadKb) {
|
|
||||||
console.log(` 🔑 Has BMAD-KB access: ${deps.agentId === 'bmad' ? 'AUTHORIZED' : '⚠️ VIOLATION!'}`);
|
|
||||||
}
|
|
||||||
} else if (options.graph) {
|
|
||||||
const graph = resolver.generateDependencyGraph();
|
|
||||||
console.log('📈 Dependency Graph:');
|
|
||||||
console.log(` Nodes: ${graph.nodes.length}`);
|
|
||||||
console.log(` Edges: ${graph.edges.length}`);
|
|
||||||
console.log('\\n Agents:');
|
|
||||||
graph.nodes.filter(n => n.type === 'agent').forEach(n => {
|
|
||||||
const bmadKbIndicator = n.hasBmadKb ? ' 🔑' : '';
|
|
||||||
console.log(` ${n.id}: ${n.label} (${n.title})${bmadKbIndicator}`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const agents = resolver.getAvailableAgents();
|
|
||||||
console.log('📊 All Agent Dependencies:');
|
|
||||||
agents.forEach(agentId => {
|
|
||||||
try {
|
|
||||||
const deps = resolver.resolveAgentDependencies(agentId, 'web');
|
|
||||||
const totalDeps = Object.values(deps.resources).reduce((sum, arr) => sum + arr.length, 0);
|
|
||||||
const hasBmadKb = deps.resources.data?.some(r => r.id === 'bmad-kb') ? ' 🔑' : '';
|
|
||||||
console.log(` ${agentId}: ${totalDeps} total dependencies${hasBmadKb}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ${agentId}: Error resolving dependencies - ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Analysis failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle unknown commands
|
|
||||||
program.on('command:*', () => {
|
|
||||||
console.error('Invalid command: %s\\nSee --help for a list of available commands.', program.args.join(' '));
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse command line arguments
|
|
||||||
program.parse();
|
|
||||||
|
|
||||||
// Show help if no command provided
|
|
||||||
if (!process.argv.slice(2).length) {
|
|
||||||
program.outputHelp();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user