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