184 lines
5.4 KiB
JavaScript
184 lines
5.4 KiB
JavaScript
const xml2js = require('xml2js');
|
|
const fs = require('fs-extra');
|
|
const path = require('node:path');
|
|
const { getProjectRoot, getSourcePath } = require('./project-root');
|
|
|
|
/**
|
|
* XML utility functions for BMAD installer
|
|
*/
|
|
class XmlHandler {
|
|
constructor() {
|
|
this.parser = new xml2js.Parser({
|
|
preserveChildrenOrder: true,
|
|
explicitChildren: true,
|
|
explicitArray: false,
|
|
trim: false,
|
|
normalizeTags: false,
|
|
attrkey: '$',
|
|
charkey: '_',
|
|
});
|
|
|
|
this.builder = new xml2js.Builder({
|
|
renderOpts: {
|
|
pretty: true,
|
|
indent: ' ',
|
|
newline: '\n',
|
|
},
|
|
xmldec: {
|
|
version: '1.0',
|
|
encoding: 'utf8',
|
|
standalone: false,
|
|
},
|
|
headless: true, // Don't add XML declaration
|
|
attrkey: '$',
|
|
charkey: '_',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load and parse the activation template
|
|
* @returns {Object} Parsed activation block
|
|
*/
|
|
async loadActivationTemplate() {
|
|
const templatePath = getSourcePath('utility', 'models', 'agent-activation-ide.xml');
|
|
|
|
try {
|
|
const xmlContent = await fs.readFile(templatePath, 'utf8');
|
|
|
|
// Parse the XML directly (file is now pure XML)
|
|
const parsed = await this.parser.parseStringPromise(xmlContent);
|
|
return parsed.activation;
|
|
} catch (error) {
|
|
console.error('Failed to load activation template:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inject activation block into agent XML content
|
|
* @param {string} agentContent - The agent file content
|
|
* @param {Object} metadata - Metadata containing module and name
|
|
* @returns {string} Modified content with activation block
|
|
*/
|
|
async injectActivation(agentContent, metadata = {}) {
|
|
try {
|
|
// Check if already has activation
|
|
if (agentContent.includes('<activation')) {
|
|
return agentContent;
|
|
}
|
|
|
|
// Extract the XML portion from markdown if needed
|
|
let xmlContent = agentContent;
|
|
let beforeXml = '';
|
|
let afterXml = '';
|
|
|
|
const xmlBlockMatch = agentContent.match(/([\s\S]*?)```xml\n([\s\S]*?)\n```([\s\S]*)/);
|
|
if (xmlBlockMatch) {
|
|
beforeXml = xmlBlockMatch[1] + '```xml\n';
|
|
xmlContent = xmlBlockMatch[2];
|
|
afterXml = '\n```' + xmlBlockMatch[3];
|
|
}
|
|
|
|
// Parse the agent XML
|
|
const parsed = await this.parser.parseStringPromise(xmlContent);
|
|
|
|
// Get the activation template
|
|
const activationBlock = await this.loadActivationTemplate();
|
|
if (!activationBlock) {
|
|
console.warn('Could not load activation template');
|
|
return agentContent;
|
|
}
|
|
|
|
// Find the agent node
|
|
if (
|
|
parsed.agent && // Insert activation as the first child
|
|
!parsed.agent.activation
|
|
) {
|
|
// Ensure proper structure
|
|
if (!parsed.agent.$$) {
|
|
parsed.agent.$$ = [];
|
|
}
|
|
|
|
// Create the activation node with proper structure
|
|
const activationNode = {
|
|
'#name': 'activation',
|
|
$: { critical: '1' },
|
|
$$: activationBlock.$$,
|
|
};
|
|
|
|
// Insert at the beginning
|
|
parsed.agent.$$.unshift(activationNode);
|
|
}
|
|
|
|
// Convert back to XML
|
|
let modifiedXml = this.builder.buildObject(parsed);
|
|
|
|
// Fix indentation - xml2js doesn't maintain our exact formatting
|
|
// Add 2-space base indentation to match our style
|
|
const lines = modifiedXml.split('\n');
|
|
const indentedLines = lines.map((line) => {
|
|
if (line.trim() === '') return line;
|
|
if (line.startsWith('<agent')) return line; // Keep agent at column 0
|
|
return ' ' + line; // Indent everything else
|
|
});
|
|
modifiedXml = indentedLines.join('\n');
|
|
|
|
// Reconstruct the full content
|
|
return beforeXml + modifiedXml + afterXml;
|
|
} catch (error) {
|
|
console.error('Error injecting activation:', error);
|
|
return agentContent;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simple string-based injection (fallback method)
|
|
* This preserves formatting better than XML parsing
|
|
*/
|
|
injectActivationSimple(agentContent, metadata = {}) {
|
|
// Check if already has activation
|
|
if (agentContent.includes('<activation')) {
|
|
return agentContent;
|
|
}
|
|
|
|
// Load template file
|
|
const templatePath = getSourcePath('utility', 'models', 'agent-activation-ide.xml');
|
|
|
|
try {
|
|
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
|
|
// The file is now pure XML, use it directly with proper indentation
|
|
// Add 2 spaces of indentation for insertion into agent
|
|
let activationBlock = templateContent
|
|
.split('\n')
|
|
.map((line) => (line ? ' ' + line : ''))
|
|
.join('\n');
|
|
|
|
// Replace {agent-filename} with actual filename if metadata provided
|
|
if (metadata.module && metadata.name) {
|
|
const agentFilename = `${metadata.module}-${metadata.name}.md`;
|
|
activationBlock = activationBlock.replace('{agent-filename}', agentFilename);
|
|
}
|
|
|
|
// Find where to insert (after <agent> tag)
|
|
const agentMatch = agentContent.match(/(<agent[^>]*>)/);
|
|
if (!agentMatch) {
|
|
return agentContent;
|
|
}
|
|
|
|
const insertPos = agentMatch.index + agentMatch[0].length;
|
|
|
|
// Insert the activation block
|
|
const before = agentContent.slice(0, insertPos);
|
|
const after = agentContent.slice(insertPos);
|
|
|
|
return before + '\n' + activationBlock + after;
|
|
} catch (error) {
|
|
console.error('Error in simple injection:', error);
|
|
return agentContent;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { XmlHandler };
|