build is back

This commit is contained in:
Brian Madison
2025-06-10 21:41:58 -05:00
parent 52b82651f7
commit fa97136909
23 changed files with 44216 additions and 15 deletions

View File

@@ -0,0 +1,145 @@
const fs = require('fs').promises;
const path = require('path');
const DependencyResolver = require('../lib/dependency-resolver');
class WebBuilder {
constructor(options = {}) {
this.rootDir = options.rootDir || process.cwd();
this.outputDirs = options.outputDirs || [
path.join(this.rootDir, 'dist'),
path.join(this.rootDir, 'web-build')
];
this.resolver = new DependencyResolver(this.rootDir);
this.templatePath = path.join(this.rootDir, 'bmad-core', 'templates', 'web-agent-startup-instructions-template.md');
}
async cleanOutputDirs() {
for (const dir of this.outputDirs) {
try {
await fs.rm(dir, { recursive: true, force: true });
console.log(`Cleaned: ${path.relative(this.rootDir, dir)}`);
} catch (error) {
// Directory might not exist, that's fine
}
}
}
async buildAgents() {
const agents = await this.resolver.listAgents();
for (const agentId of agents) {
console.log(` Building agent: ${agentId}`);
const bundle = await this.buildAgentBundle(agentId);
// Write to all output directories
for (const outputDir of this.outputDirs) {
const outputPath = path.join(outputDir, 'agents');
await fs.mkdir(outputPath, { recursive: true });
const outputFile = path.join(outputPath, `${agentId}.md`);
await fs.writeFile(outputFile, bundle, 'utf8');
}
}
console.log(`Built ${agents.length} agent bundles in ${this.outputDirs.length} locations`);
}
async buildTeams() {
const teams = await this.resolver.listTeams();
for (const teamId of teams) {
console.log(` Building team: ${teamId}`);
const bundle = await this.buildTeamBundle(teamId);
// Write to all output directories
for (const outputDir of this.outputDirs) {
const outputPath = path.join(outputDir, 'teams');
await fs.mkdir(outputPath, { recursive: true });
const outputFile = path.join(outputPath, `${teamId}.md`);
await fs.writeFile(outputFile, bundle, 'utf8');
}
}
console.log(`Built ${teams.length} team bundles in ${this.outputDirs.length} locations`);
}
async buildAgentBundle(agentId) {
const dependencies = await this.resolver.resolveAgentDependencies(agentId);
const template = await fs.readFile(this.templatePath, 'utf8');
const sections = [template];
// Add agent configuration
sections.push(this.formatSection(dependencies.agent.path, dependencies.agent.content));
// Add all dependencies
for (const resource of dependencies.resources) {
sections.push(this.formatSection(resource.path, resource.content));
}
return sections.join('\n');
}
async buildTeamBundle(teamId) {
const dependencies = await this.resolver.resolveTeamDependencies(teamId);
const template = await fs.readFile(this.templatePath, 'utf8');
const sections = [template];
// Add team configuration
sections.push(this.formatSection(dependencies.team.path, dependencies.team.content));
// Add all agents
for (const agent of dependencies.agents) {
sections.push(this.formatSection(agent.path, agent.content));
}
// Add all deduplicated resources
for (const resource of dependencies.resources) {
sections.push(this.formatSection(resource.path, resource.content));
}
return sections.join('\n');
}
formatSection(path, content) {
const separator = '====================';
return [
`${separator} START: ${path} ${separator}`,
content.trim(),
`${separator} END: ${path} ${separator}`,
''
].join('\n');
}
async validate() {
console.log('Validating agent configurations...');
const agents = await this.resolver.listAgents();
for (const agentId of agents) {
try {
await this.resolver.resolveAgentDependencies(agentId);
console.log(`${agentId}`);
} catch (error) {
console.log(`${agentId}: ${error.message}`);
throw error;
}
}
console.log('\nValidating team configurations...');
const teams = await this.resolver.listTeams();
for (const teamId of teams) {
try {
await this.resolver.resolveTeamDependencies(teamId);
console.log(`${teamId}`);
} catch (error) {
console.log(`${teamId}: ${error.message}`);
throw error;
}
}
}
listAgents() {
return this.resolver.listAgents();
}
}
module.exports = WebBuilder;

72
tools/cli.js Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env node
const { Command } = require('commander');
const WebBuilder = require('./builders/web-builder');
const path = require('path');
const program = new Command();
program
.name('bmad-build')
.description('BMAD-METHOD build tool for creating web bundles')
.version('4.0.0');
program
.command('build')
.description('Build web bundles for agents and teams')
.option('-a, --agents-only', 'Build only agent bundles')
.option('-t, --teams-only', 'Build only team bundles')
.option('--no-clean', 'Skip cleaning output directories')
.action(async (options) => {
const builder = new WebBuilder({
rootDir: process.cwd()
});
try {
if (options.clean) {
console.log('Cleaning output directories...');
await builder.cleanOutputDirs();
}
if (!options.teamsOnly) {
console.log('Building agent bundles...');
await builder.buildAgents();
}
if (!options.agentsOnly) {
console.log('Building team bundles...');
await builder.buildTeams();
}
console.log('Build completed successfully!');
} catch (error) {
console.error('Build failed:', error.message);
process.exit(1);
}
});
program
.command('list:agents')
.description('List all available agents')
.action(() => {
const builder = new WebBuilder({ rootDir: process.cwd() });
const agents = builder.listAgents();
console.log('Available agents:');
agents.forEach(agent => console.log(` - ${agent}`));
});
program
.command('validate')
.description('Validate agent and team configurations')
.action(async () => {
const builder = new WebBuilder({ rootDir: process.cwd() });
try {
await builder.validate();
console.log('All configurations are valid!');
} catch (error) {
console.error('Validation failed:', error.message);
process.exit(1);
}
});
program.parse();

View File

@@ -0,0 +1,172 @@
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
class DependencyResolver {
constructor(rootDir) {
this.rootDir = rootDir;
this.bmadCore = path.join(rootDir, 'bmad-core');
this.cache = new Map();
}
async resolveAgentDependencies(agentId) {
const agentPath = path.join(this.bmadCore, 'agents', `${agentId}.yml`);
const agentContent = await fs.readFile(agentPath, 'utf8');
const agentConfig = yaml.load(agentContent);
const dependencies = {
agent: {
id: agentId,
path: `agents#${agentId}`,
content: agentContent,
config: agentConfig
},
resources: []
};
// Resolve persona
if (agentConfig.dependencies?.persona) {
const personaId = agentConfig.dependencies.persona;
const resource = await this.loadResource('personas', personaId);
if (resource) dependencies.resources.push(resource);
}
// Resolve other dependencies
const depTypes = ['tasks', 'templates', 'checklists', 'data', 'utils'];
for (const depType of depTypes) {
const deps = agentConfig.dependencies?.[depType] || [];
for (const depId of deps) {
const resource = await this.loadResource(depType, depId);
if (resource) dependencies.resources.push(resource);
}
}
return dependencies;
}
async resolveTeamDependencies(teamId) {
const teamPath = path.join(this.bmadCore, 'agent-teams', `${teamId}.yml`);
const teamContent = await fs.readFile(teamPath, 'utf8');
const teamConfig = yaml.load(teamContent);
const dependencies = {
team: {
id: teamId,
path: `agent-teams#${teamId}`,
content: teamContent,
config: teamConfig
},
agents: [],
resources: new Map() // Use Map to deduplicate resources
};
// Always add bmad agent first if it's a team
const bmadAgent = await this.resolveAgentDependencies('bmad');
dependencies.agents.push(bmadAgent.agent);
bmadAgent.resources.forEach(res => {
dependencies.resources.set(res.path, res);
});
// Resolve all agents in the team
let agentsToResolve = teamConfig.agents || [];
// Handle wildcard "*" - include all agents
if (agentsToResolve.includes('*')) {
const allAgents = await this.listAgents();
// Remove wildcard and add all agents except those already in the list
agentsToResolve = agentsToResolve.filter(a => a !== '*');
for (const agent of allAgents) {
if (!agentsToResolve.includes(agent)) {
agentsToResolve.push(agent);
}
}
}
for (const agentId of agentsToResolve) {
if (agentId === 'bmad') continue; // Already added
const agentDeps = await this.resolveAgentDependencies(agentId);
dependencies.agents.push(agentDeps.agent);
// Add resources with deduplication
agentDeps.resources.forEach(res => {
dependencies.resources.set(res.path, res);
});
}
// Resolve workflows
for (const workflowId of teamConfig.workflows || []) {
const resource = await this.loadResource('workflows', workflowId);
if (resource) dependencies.resources.set(resource.path, resource);
}
// Convert Map back to array
dependencies.resources = Array.from(dependencies.resources.values());
return dependencies;
}
async loadResource(type, id) {
const cacheKey = `${type}#${id}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
const extensions = ['.md', '.yml', '.yaml'];
let content = null;
let filePath = null;
for (const ext of extensions) {
try {
filePath = path.join(this.bmadCore, type, `${id}${ext}`);
content = await fs.readFile(filePath, 'utf8');
break;
} catch (e) {
// Try next extension
}
}
if (!content) {
console.warn(`Resource not found: ${type}/${id}`);
return null;
}
const resource = {
type,
id,
path: `${type}#${id}`,
content
};
this.cache.set(cacheKey, resource);
return resource;
} catch (error) {
console.error(`Error loading resource ${type}/${id}:`, error.message);
return null;
}
}
async listAgents() {
try {
const files = await fs.readdir(path.join(this.bmadCore, 'agents'));
return files
.filter(f => f.endsWith('.yml'))
.map(f => f.replace('.yml', ''));
} catch (error) {
return [];
}
}
async listTeams() {
try {
const files = await fs.readdir(path.join(this.bmadCore, 'agent-teams'));
return files
.filter(f => f.endsWith('.yml'))
.map(f => f.replace('.yml', ''));
} catch (error) {
return [];
}
}
}
module.exports = DependencyResolver;