Files
BMAD-METHOD/tools/cli/lib/activation-builder.js
Brian Madison 16984c3d92 fix path bug
2025-10-04 21:33:19 -05:00

167 lines
5.6 KiB
JavaScript

const fs = require('fs-extra');
const path = require('node:path');
const { getSourcePath } = require('./project-root');
/**
* Builds activation blocks from fragments based on agent profile
*/
class ActivationBuilder {
constructor() {
this.fragmentsDir = getSourcePath('utility', 'models', 'fragments');
this.fragmentCache = new Map();
}
/**
* Load a fragment file
* @param {string} fragmentName - Name of fragment file (e.g., 'activation-init.xml')
* @returns {string} Fragment content
*/
async loadFragment(fragmentName) {
// Check cache first
if (this.fragmentCache.has(fragmentName)) {
return this.fragmentCache.get(fragmentName);
}
const fragmentPath = path.join(this.fragmentsDir, fragmentName);
if (!(await fs.pathExists(fragmentPath))) {
throw new Error(`Fragment not found: ${fragmentName}`);
}
const content = await fs.readFile(fragmentPath, 'utf8');
this.fragmentCache.set(fragmentName, content);
return content;
}
/**
* Build complete activation block based on agent profile
* @param {Object} profile - Agent profile from AgentAnalyzer
* @param {Object} metadata - Agent metadata (module, name, etc.)
* @param {Array} agentSpecificActions - Optional agent-specific critical actions
* @param {boolean} forWebBundle - Whether this is for a web bundle
* @returns {string} Complete activation block XML
*/
async buildActivation(profile, metadata = {}, agentSpecificActions = [], forWebBundle = false) {
let activation = '<activation critical="MANDATORY">\n';
// 1. Build sequential steps (use web-specific steps for web bundles)
const steps = await this.buildSteps(metadata, agentSpecificActions, forWebBundle);
activation += this.indent(steps, 2) + '\n';
// 2. Build menu handlers section with dynamic handlers
const menuHandlers = await this.loadFragment('menu-handlers.xml');
// Build extract list (comma-separated list of used attributes)
const extractList = profile.usedAttributes.join(', ');
// Build handlers (load only needed handlers)
const handlers = await this.buildHandlers(profile);
const processedHandlers = menuHandlers.replace('{DYNAMIC_EXTRACT_LIST}', extractList).replace('{DYNAMIC_HANDLERS}', handlers);
activation += '\n' + this.indent(processedHandlers, 2) + '\n';
// 3. Include rules (skip for web bundles as they're in web-bundle-activation-steps.xml)
if (!forWebBundle) {
const rules = await this.loadFragment('activation-rules.xml');
activation += this.indent(rules, 2) + '\n';
}
activation += '</activation>';
return activation;
}
/**
* Build handlers section based on profile
* @param {Object} profile - Agent profile
* @returns {string} Handlers XML
*/
async buildHandlers(profile) {
const handlerFragments = [];
for (const attrType of profile.usedAttributes) {
const fragmentName = `handler-${attrType}.xml`;
try {
const handler = await this.loadFragment(fragmentName);
handlerFragments.push(handler);
} catch {
console.warn(`Warning: Handler fragment not found: ${fragmentName}`);
}
}
return handlerFragments.join('\n');
}
/**
* Build sequential activation steps
* @param {Object} metadata - Agent metadata
* @param {Array} agentSpecificActions - Optional agent-specific actions
* @param {boolean} forWebBundle - Whether this is for a web bundle
* @returns {string} Steps XML
*/
async buildSteps(metadata = {}, agentSpecificActions = [], forWebBundle = false) {
// Use web-specific fragment for web bundles, standard fragment otherwise
const fragmentName = forWebBundle ? 'web-bundle-activation-steps.xml' : 'activation-steps.xml';
const stepsTemplate = await this.loadFragment(fragmentName);
// Extract basename from agent ID (e.g., "bmad/bmm/agents/pm.md" → "pm")
const agentBasename = metadata.id ? metadata.id.split('/').pop().replace('.md', '') : metadata.name || 'agent';
// Build agent-specific steps
let agentStepsXml = '';
let currentStepNum = 4; // Steps 1-3 are standard
if (agentSpecificActions && agentSpecificActions.length > 0) {
agentStepsXml = agentSpecificActions
.map((action) => {
const step = `<step n="${currentStepNum}">${action}</step>`;
currentStepNum++;
return step;
})
.join('\n');
}
// Calculate final step numbers
const menuStep = currentStepNum;
const haltStep = currentStepNum + 1;
const inputStep = currentStepNum + 2;
const executeStep = currentStepNum + 3;
// Replace placeholders
const processed = stepsTemplate
.replace('{agent-file-basename}', agentBasename)
.replace('{{module}}', metadata.module || 'core') // Fixed to use {{module}}
.replace('{AGENT_SPECIFIC_STEPS}', agentStepsXml)
.replace('{MENU_STEP}', menuStep.toString())
.replace('{HALT_STEP}', haltStep.toString())
.replace('{INPUT_STEP}', inputStep.toString())
.replace('{EXECUTE_STEP}', executeStep.toString());
return processed;
}
/**
* Indent XML content
* @param {string} content - Content to indent
* @param {number} spaces - Number of spaces to indent
* @returns {string} Indented content
*/
indent(content, spaces) {
const indentation = ' '.repeat(spaces);
return content
.split('\n')
.map((line) => (line ? indentation + line : line))
.join('\n');
}
/**
* Clear fragment cache (useful for testing or hot reload)
*/
clearCache() {
this.fragmentCache.clear();
}
}
module.exports = { ActivationBuilder };