|
|
|
|
@ -13,12 +13,13 @@ const { XmlHandler } = require('../../../lib/xml-handler');
|
|
|
|
|
const { DependencyResolver } = require('./dependency-resolver');
|
|
|
|
|
const { ConfigCollector } = require('./config-collector');
|
|
|
|
|
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
|
|
|
|
const { AgentPartyGenerator } = require('../../../lib/agent-party-generator');
|
|
|
|
|
const { CLIUtils } = require('../../../lib/cli-utils');
|
|
|
|
|
const { ManifestGenerator } = require('./manifest-generator');
|
|
|
|
|
const { IdeConfigManager } = require('./ide-config-manager');
|
|
|
|
|
const { CustomHandler } = require('../custom/handler');
|
|
|
|
|
const { filterCustomizationData } = require('../../../lib/agent/compiler');
|
|
|
|
|
|
|
|
|
|
// BMAD installation folder name - this is constant and should never change
|
|
|
|
|
const BMAD_FOLDER_NAME = '_bmad';
|
|
|
|
|
|
|
|
|
|
class Installer {
|
|
|
|
|
constructor() {
|
|
|
|
|
@ -34,58 +35,35 @@ class Installer {
|
|
|
|
|
this.ideConfigManager = new IdeConfigManager();
|
|
|
|
|
this.installedFiles = new Set(); // Track all installed files
|
|
|
|
|
this.ttsInjectedFiles = []; // Track files with TTS injection applied
|
|
|
|
|
this.bmadFolderName = BMAD_FOLDER_NAME;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find the bmad installation directory in a project
|
|
|
|
|
* V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml
|
|
|
|
|
* Always uses the standard _bmad folder name
|
|
|
|
|
* Also checks for legacy _cfg folder for migration
|
|
|
|
|
* @param {string} projectDir - Project directory
|
|
|
|
|
* @returns {Promise<Object>} { bmadDir: string, hasLegacyCfg: boolean }
|
|
|
|
|
*/
|
|
|
|
|
async findBmadDir(projectDir) {
|
|
|
|
|
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
|
|
|
|
|
|
|
|
|
// Check if project directory exists
|
|
|
|
|
if (!(await fs.pathExists(projectDir))) {
|
|
|
|
|
// Project doesn't exist yet, return default
|
|
|
|
|
return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
|
|
|
|
|
return { bmadDir, hasLegacyCfg: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let bmadDir = null;
|
|
|
|
|
// Check for legacy _cfg folder if bmad directory exists
|
|
|
|
|
let hasLegacyCfg = false;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const entries = await fs.readdir(projectDir, { withFileTypes: true });
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
const bmadPath = path.join(projectDir, entry.name);
|
|
|
|
|
|
|
|
|
|
// Check for current _config folder
|
|
|
|
|
const manifestPath = path.join(bmadPath, '_config', 'manifest.yaml');
|
|
|
|
|
if (await fs.pathExists(manifestPath)) {
|
|
|
|
|
// Found a V6+ installation with current _config folder
|
|
|
|
|
return { bmadDir: bmadPath, hasLegacyCfg: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for legacy _cfg folder
|
|
|
|
|
const legacyManifestPath = path.join(bmadPath, '_cfg', 'manifest.yaml');
|
|
|
|
|
if (await fs.pathExists(legacyManifestPath)) {
|
|
|
|
|
bmadDir = bmadPath;
|
|
|
|
|
hasLegacyCfg = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (await fs.pathExists(bmadDir)) {
|
|
|
|
|
const legacyCfgPath = path.join(bmadDir, '_cfg');
|
|
|
|
|
if (await fs.pathExists(legacyCfgPath)) {
|
|
|
|
|
hasLegacyCfg = true;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
console.log(chalk.red('Error reading project directory for BMAD installation detection'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we found a bmad directory (with or without legacy _cfg)
|
|
|
|
|
if (bmadDir) {
|
|
|
|
|
return { bmadDir, hasLegacyCfg };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No V6+ installation found, return default
|
|
|
|
|
// This will be used for new installations
|
|
|
|
|
return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
|
|
|
|
|
return { bmadDir, hasLegacyCfg };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -120,7 +98,7 @@ class Installer {
|
|
|
|
|
*
|
|
|
|
|
* 3. Document marker in instructions.md (if applicable)
|
|
|
|
|
*/
|
|
|
|
|
async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
|
|
|
|
|
async copyFileWithPlaceholderReplacement(sourcePath, targetPath) {
|
|
|
|
|
// List of text file extensions that should have placeholder replacement
|
|
|
|
|
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
|
|
|
|
|
const ext = path.extname(sourcePath).toLowerCase();
|
|
|
|
|
@ -285,7 +263,7 @@ class Installer {
|
|
|
|
|
// Check for already configured IDEs
|
|
|
|
|
const { Detector } = require('./detector');
|
|
|
|
|
const detector = new Detector();
|
|
|
|
|
const bmadDir = path.join(projectDir, this.bmadFolderName || 'bmad');
|
|
|
|
|
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
|
|
|
|
|
|
|
|
|
// During full reinstall, use the saved previous IDEs since bmad dir was deleted
|
|
|
|
|
// Otherwise detect from existing installation
|
|
|
|
|
@ -532,18 +510,14 @@ class Installer {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Always use _bmad as the folder name
|
|
|
|
|
const bmadFolderName = '_bmad';
|
|
|
|
|
this.bmadFolderName = bmadFolderName; // Store for use in other methods
|
|
|
|
|
|
|
|
|
|
// Store AgentVibes configuration for injection point processing
|
|
|
|
|
this.enableAgentVibes = config.enableAgentVibes || false;
|
|
|
|
|
|
|
|
|
|
// Set bmad folder name on module manager and IDE manager for placeholder replacement
|
|
|
|
|
this.moduleManager.setBmadFolderName(bmadFolderName);
|
|
|
|
|
this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
|
|
|
|
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
|
|
|
|
|
this.moduleManager.setCustomModulePaths(customModulePaths);
|
|
|
|
|
this.ideManager.setBmadFolderName(bmadFolderName);
|
|
|
|
|
this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
|
|
|
|
|
|
|
|
|
// Tool selection will be collected after we determine if it's a reinstall/update/new install
|
|
|
|
|
|
|
|
|
|
@ -553,14 +527,8 @@ class Installer {
|
|
|
|
|
// Resolve target directory (path.resolve handles platform differences)
|
|
|
|
|
const projectDir = path.resolve(config.directory);
|
|
|
|
|
|
|
|
|
|
let existingBmadDir = null;
|
|
|
|
|
let existingBmadFolderName = null;
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(projectDir)) {
|
|
|
|
|
const result = await this.findBmadDir(projectDir);
|
|
|
|
|
existingBmadDir = result.bmadDir;
|
|
|
|
|
existingBmadFolderName = path.basename(existingBmadDir);
|
|
|
|
|
}
|
|
|
|
|
// Always use the standard _bmad folder name
|
|
|
|
|
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
|
|
|
|
|
|
|
|
|
// Create a project directory if it doesn't exist (user already confirmed)
|
|
|
|
|
if (!(await fs.pathExists(projectDir))) {
|
|
|
|
|
@ -582,8 +550,6 @@ class Installer {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bmadDir = path.join(projectDir, bmadFolderName);
|
|
|
|
|
|
|
|
|
|
// Check existing installation
|
|
|
|
|
spinner.text = 'Checking for existing installation...';
|
|
|
|
|
const existingInstall = await this.detector.detect(bmadDir);
|
|
|
|
|
@ -1606,7 +1572,7 @@ class Installer {
|
|
|
|
|
const targetPath = path.join(agentsDir, fileName);
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(sourcePath)) {
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
|
|
|
|
this.installedFiles.add(targetPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -1622,7 +1588,7 @@ class Installer {
|
|
|
|
|
const targetPath = path.join(tasksDir, fileName);
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(sourcePath)) {
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
|
|
|
|
this.installedFiles.add(targetPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -1638,7 +1604,7 @@ class Installer {
|
|
|
|
|
const targetPath = path.join(toolsDir, fileName);
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(sourcePath)) {
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
|
|
|
|
this.installedFiles.add(targetPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -1654,7 +1620,7 @@ class Installer {
|
|
|
|
|
const targetPath = path.join(templatesDir, fileName);
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(sourcePath)) {
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
|
|
|
|
this.installedFiles.add(targetPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -1669,7 +1635,7 @@ class Installer {
|
|
|
|
|
await fs.ensureDir(path.dirname(targetPath));
|
|
|
|
|
|
|
|
|
|
if (await fs.pathExists(dataPath)) {
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath);
|
|
|
|
|
this.installedFiles.add(targetPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -1759,14 +1725,9 @@ class Installer {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if this is a workflow.yaml file
|
|
|
|
|
if (file.endsWith('workflow.yaml')) {
|
|
|
|
|
await fs.ensureDir(path.dirname(targetFile));
|
|
|
|
|
await this.copyWorkflowYamlStripped(sourceFile, targetFile);
|
|
|
|
|
} else {
|
|
|
|
|
// Copy the file with placeholder replacement
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad');
|
|
|
|
|
}
|
|
|
|
|
// Copy the file with placeholder replacement
|
|
|
|
|
await fs.ensureDir(path.dirname(targetFile));
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
|
|
|
|
|
|
|
|
|
|
// Track the installed file
|
|
|
|
|
this.installedFiles.add(targetFile);
|
|
|
|
|
@ -1844,7 +1805,7 @@ class Installer {
|
|
|
|
|
if (!(await fs.pathExists(customizePath))) {
|
|
|
|
|
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
|
|
|
|
|
if (await fs.pathExists(genericTemplatePath)) {
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad');
|
|
|
|
|
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
|
|
|
|
|
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
|
|
|
|
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
|
|
|
|
|
}
|
|
|
|
|
@ -1853,235 +1814,6 @@ class Installer {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Build standalone agents in bmad/agents/ directory
|
|
|
|
|
* @param {string} bmadDir - Path to bmad directory
|
|
|
|
|
* @param {string} projectDir - Path to project directory
|
|
|
|
|
*/
|
|
|
|
|
async buildStandaloneAgents(bmadDir, projectDir) {
|
|
|
|
|
const standaloneAgentsPath = path.join(bmadDir, 'agents');
|
|
|
|
|
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
|
|
|
|
|
|
|
|
|
|
// Check if standalone agents directory exists
|
|
|
|
|
if (!(await fs.pathExists(standaloneAgentsPath))) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all subdirectories in agents/
|
|
|
|
|
const agentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
|
|
|
|
|
|
|
|
|
|
for (const agentDir of agentDirs) {
|
|
|
|
|
if (!agentDir.isDirectory()) continue;
|
|
|
|
|
|
|
|
|
|
const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
|
|
|
|
|
|
|
|
|
|
// Find any .agent.yaml file in the directory
|
|
|
|
|
const files = await fs.readdir(agentDirPath);
|
|
|
|
|
const yamlFile = files.find((f) => f.endsWith('.agent.yaml'));
|
|
|
|
|
|
|
|
|
|
if (!yamlFile) continue;
|
|
|
|
|
|
|
|
|
|
const agentName = path.basename(yamlFile, '.agent.yaml');
|
|
|
|
|
const sourceYamlPath = path.join(agentDirPath, yamlFile);
|
|
|
|
|
const targetMdPath = path.join(agentDirPath, `${agentName}.md`);
|
|
|
|
|
const customizePath = path.join(cfgAgentsDir, `${agentName}.customize.yaml`);
|
|
|
|
|
|
|
|
|
|
// Check for customizations
|
|
|
|
|
const customizeExists = await fs.pathExists(customizePath);
|
|
|
|
|
let customizedFields = [];
|
|
|
|
|
|
|
|
|
|
if (customizeExists) {
|
|
|
|
|
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
|
|
|
|
const yaml = require('yaml');
|
|
|
|
|
const customizeYaml = yaml.parse(customizeContent);
|
|
|
|
|
|
|
|
|
|
// Detect what fields are customized (similar to rebuildAgentFiles)
|
|
|
|
|
if (customizeYaml) {
|
|
|
|
|
if (customizeYaml.persona) {
|
|
|
|
|
for (const [key, value] of Object.entries(customizeYaml.persona)) {
|
|
|
|
|
if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
|
|
|
|
|
customizedFields.push(`persona.${key}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (customizeYaml.agent?.metadata) {
|
|
|
|
|
for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
|
|
|
|
|
if (value !== '' && value !== null) {
|
|
|
|
|
customizedFields.push(`metadata.${key}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
|
|
|
|
|
customizedFields.push('critical_actions');
|
|
|
|
|
}
|
|
|
|
|
if (customizeYaml.menu && customizeYaml.menu.length > 0) {
|
|
|
|
|
customizedFields.push('menu');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build YAML to XML .md
|
|
|
|
|
let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
|
|
|
|
|
includeMetadata: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
|
|
|
|
|
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
|
|
|
|
|
|
|
|
|
|
// Process TTS injection points (pass targetPath for tracking)
|
|
|
|
|
xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
|
|
|
|
|
|
|
|
|
|
// Write the built .md file with POSIX-compliant final newline
|
|
|
|
|
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
|
|
|
|
|
await fs.writeFile(targetMdPath, content, 'utf8');
|
|
|
|
|
|
|
|
|
|
// Display result
|
|
|
|
|
if (customizedFields.length > 0) {
|
|
|
|
|
console.log(chalk.dim(` Built standalone agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
|
|
|
|
|
} else {
|
|
|
|
|
console.log(chalk.dim(` Built standalone agent: ${agentName}.md`));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Rebuild agent files from installer source (for compile command)
|
|
|
|
|
* @param {string} modulePath - Path to module in bmad/ installation
|
|
|
|
|
* @param {string} moduleName - Module name
|
|
|
|
|
*/
|
|
|
|
|
async rebuildAgentFiles(modulePath, moduleName) {
|
|
|
|
|
// Get source agents directory from installer
|
|
|
|
|
const sourceAgentsPath =
|
|
|
|
|
moduleName === 'core' ? path.join(getModulePath('core'), 'agents') : path.join(getSourcePath(`modules/${moduleName}`), 'agents');
|
|
|
|
|
|
|
|
|
|
if (!(await fs.pathExists(sourceAgentsPath))) {
|
|
|
|
|
return; // No source agents to rebuild
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine project directory (parent of bmad/ directory)
|
|
|
|
|
const bmadDir = path.dirname(modulePath);
|
|
|
|
|
const projectDir = path.dirname(bmadDir);
|
|
|
|
|
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
|
|
|
|
|
const targetAgentsPath = path.join(modulePath, 'agents');
|
|
|
|
|
|
|
|
|
|
// Ensure target directory exists
|
|
|
|
|
await fs.ensureDir(targetAgentsPath);
|
|
|
|
|
|
|
|
|
|
// Get all YAML agent files from source
|
|
|
|
|
const sourceFiles = await fs.readdir(sourceAgentsPath);
|
|
|
|
|
|
|
|
|
|
for (const file of sourceFiles) {
|
|
|
|
|
if (file.endsWith('.agent.yaml')) {
|
|
|
|
|
const agentName = file.replace('.agent.yaml', '');
|
|
|
|
|
const sourceYamlPath = path.join(sourceAgentsPath, file);
|
|
|
|
|
const targetMdPath = path.join(targetAgentsPath, `${agentName}.md`);
|
|
|
|
|
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
|
|
|
|
|
|
|
|
|
|
// Check for customizations
|
|
|
|
|
const customizeExists = await fs.pathExists(customizePath);
|
|
|
|
|
let customizedFields = [];
|
|
|
|
|
|
|
|
|
|
if (customizeExists) {
|
|
|
|
|
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
|
|
|
|
const yaml = require('yaml');
|
|
|
|
|
const customizeYaml = yaml.parse(customizeContent);
|
|
|
|
|
|
|
|
|
|
// Detect what fields are customized
|
|
|
|
|
if (customizeYaml) {
|
|
|
|
|
if (customizeYaml.persona) {
|
|
|
|
|
for (const [key, value] of Object.entries(customizeYaml.persona)) {
|
|
|
|
|
if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
|
|
|
|
|
customizedFields.push(`persona.${key}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (customizeYaml.agent?.metadata) {
|
|
|
|
|
for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
|
|
|
|
|
if (value !== '' && value !== null) {
|
|
|
|
|
customizedFields.push(`metadata.${key}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
|
|
|
|
|
customizedFields.push('critical_actions');
|
|
|
|
|
}
|
|
|
|
|
if (customizeYaml.memories && customizeYaml.memories.length > 0) {
|
|
|
|
|
customizedFields.push('memories');
|
|
|
|
|
}
|
|
|
|
|
if (customizeYaml.menu && customizeYaml.menu.length > 0) {
|
|
|
|
|
customizedFields.push('menu');
|
|
|
|
|
}
|
|
|
|
|
if (customizeYaml.prompts && customizeYaml.prompts.length > 0) {
|
|
|
|
|
customizedFields.push('prompts');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read the YAML content
|
|
|
|
|
const yamlContent = await fs.readFile(sourceYamlPath, 'utf8');
|
|
|
|
|
|
|
|
|
|
// Read customize content if exists
|
|
|
|
|
let customizeData = {};
|
|
|
|
|
if (customizeExists) {
|
|
|
|
|
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
|
|
|
|
const yaml = require('yaml');
|
|
|
|
|
customizeData = yaml.parse(customizeContent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build agent answers from customize data (filter empty values)
|
|
|
|
|
const answers = {};
|
|
|
|
|
if (customizeData.persona) {
|
|
|
|
|
Object.assign(answers, filterCustomizationData(customizeData.persona));
|
|
|
|
|
}
|
|
|
|
|
if (customizeData.agent?.metadata) {
|
|
|
|
|
const filteredMetadata = filterCustomizationData(customizeData.agent.metadata);
|
|
|
|
|
if (Object.keys(filteredMetadata).length > 0) {
|
|
|
|
|
Object.assign(answers, { metadata: filteredMetadata });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (customizeData.critical_actions && customizeData.critical_actions.length > 0) {
|
|
|
|
|
answers.critical_actions = customizeData.critical_actions;
|
|
|
|
|
}
|
|
|
|
|
if (customizeData.memories && customizeData.memories.length > 0) {
|
|
|
|
|
answers.memories = customizeData.memories;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
|
|
|
|
|
let coreConfig = {};
|
|
|
|
|
if (await fs.pathExists(coreConfigPath)) {
|
|
|
|
|
const yaml = require('yaml');
|
|
|
|
|
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
|
|
|
|
|
coreConfig = yaml.parse(coreConfigContent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compile using the same compiler as initial installation
|
|
|
|
|
const { compileAgent } = require('../../../lib/agent/compiler');
|
|
|
|
|
const result = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
|
|
|
|
|
config: coreConfig,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Check if compilation succeeded
|
|
|
|
|
if (!result || !result.xml) {
|
|
|
|
|
throw new Error(`Failed to compile agent ${agentName}: No XML returned from compiler`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Replace _bmad with actual folder name if needed
|
|
|
|
|
const finalXml = result.xml.replaceAll('_bmad', path.basename(bmadDir));
|
|
|
|
|
|
|
|
|
|
// Write the rebuilt .md file with POSIX-compliant final newline
|
|
|
|
|
const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n';
|
|
|
|
|
await fs.writeFile(targetMdPath, content, 'utf8');
|
|
|
|
|
|
|
|
|
|
// Display result with customizations if any
|
|
|
|
|
if (customizedFields.length > 0) {
|
|
|
|
|
console.log(chalk.dim(` Rebuilt agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
|
|
|
|
|
} else {
|
|
|
|
|
console.log(chalk.dim(` Rebuilt agent: ${agentName}.md`));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Private: Update core
|
|
|
|
|
*/
|
|
|
|
|
@ -2677,190 +2409,6 @@ class Installer {
|
|
|
|
|
return { customFiles, modifiedFiles };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Private: Create agent configuration files
|
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
|
|
|
|
* @param {Object} userInfo - User information including name and language
|
|
|
|
|
*/
|
|
|
|
|
async createAgentConfigs(bmadDir, userInfo = null) {
|
|
|
|
|
const agentConfigDir = path.join(bmadDir, '_config', 'agents');
|
|
|
|
|
await fs.ensureDir(agentConfigDir);
|
|
|
|
|
|
|
|
|
|
// Get all agents from all modules
|
|
|
|
|
const agents = [];
|
|
|
|
|
const agentDetails = []; // For manifest generation
|
|
|
|
|
|
|
|
|
|
// Check modules for agents (including core)
|
|
|
|
|
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
if (entry.isDirectory() && entry.name !== '_config') {
|
|
|
|
|
const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents');
|
|
|
|
|
if (await fs.pathExists(moduleAgentsPath)) {
|
|
|
|
|
const agentFiles = await fs.readdir(moduleAgentsPath);
|
|
|
|
|
for (const agentFile of agentFiles) {
|
|
|
|
|
if (agentFile.endsWith('.md')) {
|
|
|
|
|
const agentPath = path.join(moduleAgentsPath, agentFile);
|
|
|
|
|
const agentContent = await fs.readFile(agentPath, 'utf8');
|
|
|
|
|
|
|
|
|
|
// Skip agents with localskip="true"
|
|
|
|
|
const hasLocalSkip = agentContent.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
|
|
|
|
|
if (hasLocalSkip) {
|
|
|
|
|
continue; // Skip this agent - it should not have been installed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const agentName = path.basename(agentFile, '.md');
|
|
|
|
|
|
|
|
|
|
// Extract any nodes with agentConfig="true"
|
|
|
|
|
const agentConfigNodes = this.extractAgentConfigNodes(agentContent);
|
|
|
|
|
|
|
|
|
|
agents.push({
|
|
|
|
|
name: agentName,
|
|
|
|
|
module: entry.name,
|
|
|
|
|
agentConfigNodes: agentConfigNodes,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Use shared AgentPartyGenerator to extract details
|
|
|
|
|
let details = AgentPartyGenerator.extractAgentDetails(agentContent, entry.name, agentName);
|
|
|
|
|
|
|
|
|
|
// Apply config overrides if they exist
|
|
|
|
|
if (details) {
|
|
|
|
|
const configPath = path.join(agentConfigDir, `${entry.name}-${agentName}.md`);
|
|
|
|
|
if (await fs.pathExists(configPath)) {
|
|
|
|
|
const configContent = await fs.readFile(configPath, 'utf8');
|
|
|
|
|
details = AgentPartyGenerator.applyConfigOverrides(details, configContent);
|
|
|
|
|
}
|
|
|
|
|
agentDetails.push(details);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create config file for each agent
|
|
|
|
|
let createdCount = 0;
|
|
|
|
|
let skippedCount = 0;
|
|
|
|
|
|
|
|
|
|
// Load agent config template
|
|
|
|
|
const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md');
|
|
|
|
|
const templateContent = await fs.readFile(templatePath, 'utf8');
|
|
|
|
|
|
|
|
|
|
for (const agent of agents) {
|
|
|
|
|
const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`);
|
|
|
|
|
|
|
|
|
|
// Skip if config file already exists (preserve custom configurations)
|
|
|
|
|
if (await fs.pathExists(configPath)) {
|
|
|
|
|
skippedCount++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build config content header
|
|
|
|
|
let configContent = `# Agent Config: ${agent.name}\n\n`;
|
|
|
|
|
|
|
|
|
|
// Process template and add agent-specific config nodes
|
|
|
|
|
let processedTemplate = templateContent;
|
|
|
|
|
|
|
|
|
|
// Replace {core:user_name} placeholder with actual user name if available
|
|
|
|
|
if (userInfo && userInfo.userName) {
|
|
|
|
|
processedTemplate = processedTemplate.replaceAll('{core:user_name}', userInfo.userName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Replace {core:communication_language} placeholder with actual language if available
|
|
|
|
|
if (userInfo && userInfo.responseLanguage) {
|
|
|
|
|
processedTemplate = processedTemplate.replaceAll('{core:communication_language}', userInfo.responseLanguage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If this agent has agentConfig nodes, add them after the existing comment
|
|
|
|
|
if (agent.agentConfigNodes && agent.agentConfigNodes.length > 0) {
|
|
|
|
|
// Find the agent-specific configuration nodes comment
|
|
|
|
|
const commentPattern = /(\s*<!-- Agent-specific configuration nodes -->)/;
|
|
|
|
|
const commentMatch = processedTemplate.match(commentPattern);
|
|
|
|
|
|
|
|
|
|
if (commentMatch) {
|
|
|
|
|
// Add nodes right after the comment
|
|
|
|
|
let agentSpecificNodes = '';
|
|
|
|
|
for (const node of agent.agentConfigNodes) {
|
|
|
|
|
agentSpecificNodes += `\n ${node}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
processedTemplate = processedTemplate.replace(commentPattern, `$1${agentSpecificNodes}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
configContent += processedTemplate;
|
|
|
|
|
|
|
|
|
|
// Ensure POSIX-compliant final newline
|
|
|
|
|
if (!configContent.endsWith('\n')) {
|
|
|
|
|
configContent += '\n';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await fs.writeFile(configPath, configContent, 'utf8');
|
|
|
|
|
this.installedFiles.add(configPath); // Track agent config files
|
|
|
|
|
createdCount++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate agent manifest with overrides applied
|
|
|
|
|
await this.generateAgentManifest(bmadDir, agentDetails);
|
|
|
|
|
|
|
|
|
|
return { total: agents.length, created: createdCount, skipped: skippedCount };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate agent manifest XML file
|
|
|
|
|
* @param {string} bmadDir - BMAD installation directory
|
|
|
|
|
* @param {Array} agentDetails - Array of agent details
|
|
|
|
|
*/
|
|
|
|
|
async generateAgentManifest(bmadDir, agentDetails) {
|
|
|
|
|
const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
|
|
|
|
|
await AgentPartyGenerator.writeAgentParty(manifestPath, agentDetails, { forWeb: false });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract nodes with agentConfig="true" from agent content
|
|
|
|
|
* @param {string} content - Agent file content
|
|
|
|
|
* @returns {Array} Array of XML nodes that should be added to agent config
|
|
|
|
|
*/
|
|
|
|
|
extractAgentConfigNodes(content) {
|
|
|
|
|
const nodes = [];
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Find all XML nodes with agentConfig="true"
|
|
|
|
|
// Match self-closing tags and tags with content
|
|
|
|
|
const selfClosingPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*\/>/g;
|
|
|
|
|
const withContentPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*>([\s\S]*?)<\/\1>/g;
|
|
|
|
|
|
|
|
|
|
// Extract self-closing tags
|
|
|
|
|
let match;
|
|
|
|
|
while ((match = selfClosingPattern.exec(content)) !== null) {
|
|
|
|
|
// Extract just the tag without children (structure only)
|
|
|
|
|
const tagMatch = match[0].match(/<([a-zA-Z][a-zA-Z0-9_-]*)([^>]*)\/>/);
|
|
|
|
|
if (tagMatch) {
|
|
|
|
|
const tagName = tagMatch[1];
|
|
|
|
|
const attributes = tagMatch[2].replace(/\s*agentConfig="true"/, ''); // Remove agentConfig attribute
|
|
|
|
|
nodes.push(`<${tagName}${attributes}></${tagName}>`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract tags with content
|
|
|
|
|
while ((match = withContentPattern.exec(content)) !== null) {
|
|
|
|
|
const fullMatch = match[0];
|
|
|
|
|
const tagName = match[1];
|
|
|
|
|
|
|
|
|
|
// Extract opening tag with attributes (removing agentConfig="true")
|
|
|
|
|
const openingTagMatch = fullMatch.match(new RegExp(`<${tagName}([^>]*)>`));
|
|
|
|
|
if (openingTagMatch) {
|
|
|
|
|
const attributes = openingTagMatch[1].replace(/\s*agentConfig="true"/, '');
|
|
|
|
|
// Add empty node structure (no children)
|
|
|
|
|
nodes.push(`<${tagName}${attributes}></${tagName}>`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error extracting agentConfig nodes:', error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nodes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle missing custom module sources interactively
|
|
|
|
|
* @param {Map} customModuleSources - Map of custom module ID to info
|
|
|
|
|
@ -2999,7 +2547,7 @@ class Installer {
|
|
|
|
|
await this.manifest.addCustomModule(bmadDir, missing.info);
|
|
|
|
|
|
|
|
|
|
validCustomModules.push({
|
|
|
|
|
id: moduleId,
|
|
|
|
|
id: missing.id,
|
|
|
|
|
name: missing.name,
|
|
|
|
|
path: resolvedPath,
|
|
|
|
|
info: missing.info,
|
|
|
|
|
@ -3013,7 +2561,7 @@ class Installer {
|
|
|
|
|
case 'remove': {
|
|
|
|
|
// Extra confirmation for destructive remove
|
|
|
|
|
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
|
|
|
|
|
console.log(chalk.red(` Module location: ${path.join(bmadDir, moduleId)}`));
|
|
|
|
|
console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
|
|
|
|
|
|
|
|
|
|
const { confirm } = await inquirer.prompt([
|
|
|
|
|
{
|
|
|
|
|
|