BMAD-METHOD/tools/builders/web-builder.js

321 lines
12 KiB
JavaScript

const fs = require('node:fs').promises;
const path = require('node:path');
const DependencyResolver = require('../lib/dependency-resolver');
const yamlUtilities = require('../lib/yaml-utils');
class WebBuilder {
constructor(options = {}) {
this.rootDir = options.rootDir || process.cwd();
this.outputDirs = options.outputDirs || [path.join(this.rootDir, 'dist')];
this.resolver = new DependencyResolver(this.rootDir);
this.templatePath = path.join(
this.rootDir,
'tools',
'md-assets',
'web-agent-startup-instructions.md',
);
}
parseYaml(content) {
const yaml = require('js-yaml');
return yaml.load(content);
}
convertToWebPath(filePath, bundleRoot = 'bmad-core') {
// Convert absolute paths to web bundle paths with dot prefix
// All resources get installed under the bundle root, so use that path
const relativePath = path.relative(this.rootDir, filePath);
const pathParts = relativePath.split(path.sep);
let resourcePath;
if (pathParts[0] === 'expansion-packs') {
// For expansion packs, remove 'expansion-packs/packname' and use the rest
resourcePath = pathParts.slice(2).join('/');
} else {
// For bmad-core, common, etc., remove the first part
resourcePath = pathParts.slice(1).join('/');
}
return `.${bundleRoot}/${resourcePath}`;
}
generateWebInstructions(bundleType, packName = null) {
// Generate dynamic web instructions based on bundle type
const rootExample = packName ? `.${packName}` : '.bmad-core';
const examplePath = packName
? `.${packName}/folder/filename.md`
: '.bmad-core/folder/filename.md';
const personasExample = packName
? `.${packName}/personas/analyst.md`
: '.bmad-core/personas/analyst.md';
const tasksExample = packName
? `.${packName}/tasks/create-story.md`
: '.bmad-core/tasks/create-story.md';
const utilitiesExample = packName
? `.${packName}/utils/template-format.md`
: '.bmad-core/utils/template-format.md';
const tasksReference = packName
? `.${packName}/tasks/create-story.md`
: '.bmad-core/tasks/create-story.md';
return `# Web Agent Bundle Instructions
You are now operating as a specialized AI agent from the BMad-Method framework. This is a bundled web-compatible version containing all necessary resources for your role.
## Important Instructions
1. **Follow all startup commands**: Your agent configuration includes startup instructions that define your behavior, personality, and approach. These MUST be followed exactly.
2. **Resource Navigation**: This bundle contains all resources you need. Resources are marked with tags like:
- \`==================== START: ${examplePath} ====================\`
- \`==================== END: ${examplePath} ====================\`
When you need to reference a resource mentioned in your instructions:
- Look for the corresponding START/END tags
- The format is always the full path with dot prefix (e.g., \`${personasExample}\`, \`${tasksExample}\`)
- If a section is specified (e.g., \`{root}/tasks/create-story.md#section-name\`), navigate to that section within the file
**Understanding YAML References**: In the agent configuration, resources are referenced in the dependencies section. For example:
\`\`\`yaml
dependencies:
utils:
- template-format
tasks:
- create-story
\`\`\`
These references map directly to bundle sections:
- \`utils: template-format\` → Look for \`==================== START: ${utilitiesExample} ====================\`
- \`tasks: create-story\` → Look for \`==================== START: ${tasksReference} ====================\`
3. **Execution Context**: You are operating in a web environment. All your capabilities and knowledge are contained within this bundle. Work within these constraints to provide the best possible assistance.
4. **Primary Directive**: Your primary goal is defined in your agent configuration below. Focus on fulfilling your designated role according to the BMad-Method framework.
---
`;
}
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) {
console.debug(`Failed to clean directory ${dir}:`, error.message);
// 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}.txt`);
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}.txt`);
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 = this.generateWebInstructions('agent');
const sections = [template];
// Add agent configuration
let agentContent = await fs.readFile(path.join(packDir, 'agents', `.md`), 'utf8');
if (language) {
const yaml = require('js-yaml');
const yamlContent = yamlUtilities.extractYamlFromAgent(agentContent);
if (yamlContent) {
const agentConfig = yaml.load(yamlContent);
if (!agentConfig['activation-instructions']) {
agentConfig['activation-instructions'] = [];
}
agentConfig['activation-instructions'].push(`You must reply in .`);
const newYamlContent = yaml.dump(agentConfig);
agentContent = agentContent.replace(yamlContent, newYamlContent);
}
}
const agentPath = path.join(packDir, 'agents', `.md`);
const agentWebPath = this.convertToWebPath(agentPath, packName);
sections.push(this.formatSection(agentWebPath, agentContent, packName));
// Resolve and add agent dependencies
const yamlContent = yamlUtilities.extractYamlFromAgent(agentContent);
if (yamlContent) {
try {
const yaml = require('js-yaml');
const agentConfig = yaml.load(yamlContent);
if (agentConfig.dependencies) {
// Add resources, first try expansion pack, then core
for (const [resourceType, resources] of Object.entries(agentConfig.dependencies)) {
if (Array.isArray(resources)) {
for (const resourceName of resources) {
let found = false;
// Try expansion pack first
const resourcePath = path.join(packDir, resourceType, resourceName);
try {
const resourceContent = await fs.readFile(resourcePath, 'utf8');
const resourceWebPath = this.convertToWebPath(resourcePath, packName);
sections.push(this.formatSection(resourceWebPath, resourceContent, packName));
found = true;
} catch {
// Not in expansion pack, continue
}
// If not found in expansion pack, try core
if (!found) {
const corePath = path.join(this.rootDir, 'bmad-core', resourceType, resourceName);
try {
const coreContent = await fs.readFile(corePath, 'utf8');
const coreWebPath = this.convertToWebPath(corePath, packName);
sections.push(this.formatSection(coreWebPath, coreContent, packName));
found = true;
} catch {
// Not in core either, continue
}
}
// If not found in core, try common folder
if (!found) {
const commonPath = path.join(this.rootDir, 'common', resourceType, resourceName);
try {
const commonContent = await fs.readFile(commonPath, 'utf8');
const commonWebPath = this.convertToWebPath(commonPath, packName);
sections.push(this.formatSection(commonWebPath, commonContent, packName));
found = true;
} catch {
// Not in common either, continue
}
}
if (!found) {
console.warn(
` ⚠ Dependency ${resourceType}#${resourceName} not found in expansion pack or core`,
);
}
}
}
}
}
} catch (error) {
console.debug(`Failed to parse agent YAML for ${agentName}:`, error.message);
}
}
return sections.join('\n');
}
async buildExpansionTeamBundle(packName, packDir, teamConfigPath, language = null) {
const template = this.generateWebInstructions('expansion-team', packName);
const sections = [template];
// Add team configuration and parse to get agent list
const teamContent = await fs.readFile(teamConfigPath, 'utf8');
const teamFileName = path.basename(teamConfigPath, '.yaml');
const teamConfig = this.parseYaml(teamContent);
const teamWebPath = this.convertToWebPath(teamConfigPath, packName);
sections.push(this.formatSection(teamWebPath, teamContent, packName));
// Get list of expansion pack agents
const expansionAgents = new Set();
const agentsDir = path.join(packDir, 'agents');
try {
const agentFiles = await fs.readdir(agentsDir);
for (const agentFile of agentFiles.filter((f) => f.endsWith('.md'))) {
const agentName = agentFile.replace('.md', '');
expansionAgents.add(agentName);
}
} catch {
console.warn(` ⚠ No agents directory found in ${packName}`);
}
// Build a map of all available expansion pack resources for override checking
const expansionResources = new Map();
const resourceDirectories = ['templates', 'tasks', 'checklists', 'workflows', 'data'];
for (const resourceDir of resourceDirectories) {
const resourcePath = path.join(packDir, resourceDir);
try {
const resourceFiles = await fs.readdir(resourcePath);
for (const resourceFile of resourceFiles.filter(
(f) => f.endsWith('.md') || f.endsWith('.yaml') || f.endsWith('.csv')
)) {
const filePath = path.join(resourcePath, resourceFile);
const fileContent = await fs.readFile(filePath, 'utf8');
const fileName = resourceFile.replace(/\.(md|yaml)$/, '');
// Only add if not already included as a dependency
const resourceKey = `${resourceDir}#${fileName}`;
if (!allDependencies.has(resourceKey)) {
const fullResourcePath = path.join(resourcePath, resourceFile);
const resourceWebPath = this.convertToWebPath(fullResourcePath, packName);
sections.push(this.formatSection(resourceWebPath, fileContent, packName));
}
}
} catch {
// Directory might not exist, that's fine
}
}
return sections.join('\n');
}
async listExpansionPacks() {
const expansionPacksDir = path.join(this.rootDir, 'expansion-packs');
try {
const entries = await fs.readdir(expansionPacksDir, { withFileTypes: true });
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
} catch {
console.warn('No expansion-packs directory found');
return [];
}
}
listAgents() {
return this.resolver.listAgents();
}
}
module.exports = WebBuilder;