fixed _bmad folder stutter with agent custom files
This commit is contained in:
parent
363915b0c6
commit
23f650ff4d
|
|
@ -13,12 +13,13 @@ const { XmlHandler } = require('../../../lib/xml-handler');
|
||||||
const { DependencyResolver } = require('./dependency-resolver');
|
const { DependencyResolver } = require('./dependency-resolver');
|
||||||
const { ConfigCollector } = require('./config-collector');
|
const { ConfigCollector } = require('./config-collector');
|
||||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||||
const { AgentPartyGenerator } = require('../../../lib/agent-party-generator');
|
|
||||||
const { CLIUtils } = require('../../../lib/cli-utils');
|
const { CLIUtils } = require('../../../lib/cli-utils');
|
||||||
const { ManifestGenerator } = require('./manifest-generator');
|
const { ManifestGenerator } = require('./manifest-generator');
|
||||||
const { IdeConfigManager } = require('./ide-config-manager');
|
const { IdeConfigManager } = require('./ide-config-manager');
|
||||||
const { CustomHandler } = require('../custom/handler');
|
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 {
|
class Installer {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -34,58 +35,35 @@ class Installer {
|
||||||
this.ideConfigManager = new IdeConfigManager();
|
this.ideConfigManager = new IdeConfigManager();
|
||||||
this.installedFiles = new Set(); // Track all installed files
|
this.installedFiles = new Set(); // Track all installed files
|
||||||
this.ttsInjectedFiles = []; // Track files with TTS injection applied
|
this.ttsInjectedFiles = []; // Track files with TTS injection applied
|
||||||
|
this.bmadFolderName = BMAD_FOLDER_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the bmad installation directory in a project
|
* 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
|
* Also checks for legacy _cfg folder for migration
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
* @returns {Promise<Object>} { bmadDir: string, hasLegacyCfg: boolean }
|
* @returns {Promise<Object>} { bmadDir: string, hasLegacyCfg: boolean }
|
||||||
*/
|
*/
|
||||||
async findBmadDir(projectDir) {
|
async findBmadDir(projectDir) {
|
||||||
|
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
||||||
|
|
||||||
// Check if project directory exists
|
// Check if project directory exists
|
||||||
if (!(await fs.pathExists(projectDir))) {
|
if (!(await fs.pathExists(projectDir))) {
|
||||||
// Project doesn't exist yet, return default
|
// 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;
|
let hasLegacyCfg = false;
|
||||||
|
if (await fs.pathExists(bmadDir)) {
|
||||||
try {
|
const legacyCfgPath = path.join(bmadDir, '_cfg');
|
||||||
const entries = await fs.readdir(projectDir, { withFileTypes: true });
|
if (await fs.pathExists(legacyCfgPath)) {
|
||||||
for (const entry of entries) {
|
hasLegacyCfg = true;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
console.log(chalk.red('Error reading project directory for BMAD installation detection'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we found a bmad directory (with or without legacy _cfg)
|
return { bmadDir, hasLegacyCfg };
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -120,7 +98,7 @@ class Installer {
|
||||||
*
|
*
|
||||||
* 3. Document marker in instructions.md (if applicable)
|
* 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
|
// 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 textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
|
||||||
const ext = path.extname(sourcePath).toLowerCase();
|
const ext = path.extname(sourcePath).toLowerCase();
|
||||||
|
|
@ -285,7 +263,7 @@ class Installer {
|
||||||
// Check for already configured IDEs
|
// Check for already configured IDEs
|
||||||
const { Detector } = require('./detector');
|
const { Detector } = require('./detector');
|
||||||
const detector = new 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
|
// During full reinstall, use the saved previous IDEs since bmad dir was deleted
|
||||||
// Otherwise detect from existing installation
|
// 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
|
// Store AgentVibes configuration for injection point processing
|
||||||
this.enableAgentVibes = config.enableAgentVibes || false;
|
this.enableAgentVibes = config.enableAgentVibes || false;
|
||||||
|
|
||||||
// Set bmad folder name on module manager and IDE manager for placeholder replacement
|
// 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.setCoreConfig(moduleConfigs.core || {});
|
||||||
this.moduleManager.setCustomModulePaths(customModulePaths);
|
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
|
// 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)
|
// Resolve target directory (path.resolve handles platform differences)
|
||||||
const projectDir = path.resolve(config.directory);
|
const projectDir = path.resolve(config.directory);
|
||||||
|
|
||||||
let existingBmadDir = null;
|
// Always use the standard _bmad folder name
|
||||||
let existingBmadFolderName = null;
|
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
|
||||||
|
|
||||||
if (await fs.pathExists(projectDir)) {
|
|
||||||
const result = await this.findBmadDir(projectDir);
|
|
||||||
existingBmadDir = result.bmadDir;
|
|
||||||
existingBmadFolderName = path.basename(existingBmadDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a project directory if it doesn't exist (user already confirmed)
|
// Create a project directory if it doesn't exist (user already confirmed)
|
||||||
if (!(await fs.pathExists(projectDir))) {
|
if (!(await fs.pathExists(projectDir))) {
|
||||||
|
|
@ -582,8 +550,6 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bmadDir = path.join(projectDir, bmadFolderName);
|
|
||||||
|
|
||||||
// Check existing installation
|
// Check existing installation
|
||||||
spinner.text = 'Checking for existing installation...';
|
spinner.text = 'Checking for existing installation...';
|
||||||
const existingInstall = await this.detector.detect(bmadDir);
|
const existingInstall = await this.detector.detect(bmadDir);
|
||||||
|
|
@ -1606,7 +1572,7 @@ class Installer {
|
||||||
const targetPath = path.join(agentsDir, fileName);
|
const targetPath = path.join(agentsDir, fileName);
|
||||||
|
|
||||||
if (await fs.pathExists(sourcePath)) {
|
if (await fs.pathExists(sourcePath)) {
|
||||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
||||||
this.installedFiles.add(targetPath);
|
this.installedFiles.add(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1622,7 +1588,7 @@ class Installer {
|
||||||
const targetPath = path.join(tasksDir, fileName);
|
const targetPath = path.join(tasksDir, fileName);
|
||||||
|
|
||||||
if (await fs.pathExists(sourcePath)) {
|
if (await fs.pathExists(sourcePath)) {
|
||||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
||||||
this.installedFiles.add(targetPath);
|
this.installedFiles.add(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1638,7 +1604,7 @@ class Installer {
|
||||||
const targetPath = path.join(toolsDir, fileName);
|
const targetPath = path.join(toolsDir, fileName);
|
||||||
|
|
||||||
if (await fs.pathExists(sourcePath)) {
|
if (await fs.pathExists(sourcePath)) {
|
||||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
||||||
this.installedFiles.add(targetPath);
|
this.installedFiles.add(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1654,7 +1620,7 @@ class Installer {
|
||||||
const targetPath = path.join(templatesDir, fileName);
|
const targetPath = path.join(templatesDir, fileName);
|
||||||
|
|
||||||
if (await fs.pathExists(sourcePath)) {
|
if (await fs.pathExists(sourcePath)) {
|
||||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
||||||
this.installedFiles.add(targetPath);
|
this.installedFiles.add(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1669,7 +1635,7 @@ class Installer {
|
||||||
await fs.ensureDir(path.dirname(targetPath));
|
await fs.ensureDir(path.dirname(targetPath));
|
||||||
|
|
||||||
if (await fs.pathExists(dataPath)) {
|
if (await fs.pathExists(dataPath)) {
|
||||||
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
|
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath);
|
||||||
this.installedFiles.add(targetPath);
|
this.installedFiles.add(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1759,14 +1725,9 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a workflow.yaml file
|
// Copy the file with placeholder replacement
|
||||||
if (file.endsWith('workflow.yaml')) {
|
await fs.ensureDir(path.dirname(targetFile));
|
||||||
await fs.ensureDir(path.dirname(targetFile));
|
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
|
||||||
await this.copyWorkflowYamlStripped(sourceFile, targetFile);
|
|
||||||
} else {
|
|
||||||
// Copy the file with placeholder replacement
|
|
||||||
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track the installed file
|
// Track the installed file
|
||||||
this.installedFiles.add(targetFile);
|
this.installedFiles.add(targetFile);
|
||||||
|
|
@ -1844,7 +1805,7 @@ class Installer {
|
||||||
if (!(await fs.pathExists(customizePath))) {
|
if (!(await fs.pathExists(customizePath))) {
|
||||||
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
|
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
|
||||||
if (await fs.pathExists(genericTemplatePath)) {
|
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') {
|
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
||||||
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
|
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
|
* Private: Update core
|
||||||
*/
|
*/
|
||||||
|
|
@ -2677,190 +2409,6 @@ class Installer {
|
||||||
return { customFiles, modifiedFiles };
|
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
|
* Handle missing custom module sources interactively
|
||||||
* @param {Map} customModuleSources - Map of custom module ID to info
|
* @param {Map} customModuleSources - Map of custom module ID to info
|
||||||
|
|
@ -2999,7 +2547,7 @@ class Installer {
|
||||||
await this.manifest.addCustomModule(bmadDir, missing.info);
|
await this.manifest.addCustomModule(bmadDir, missing.info);
|
||||||
|
|
||||||
validCustomModules.push({
|
validCustomModules.push({
|
||||||
id: moduleId,
|
id: missing.id,
|
||||||
name: missing.name,
|
name: missing.name,
|
||||||
path: resolvedPath,
|
path: resolvedPath,
|
||||||
info: missing.info,
|
info: missing.info,
|
||||||
|
|
@ -3013,7 +2561,7 @@ class Installer {
|
||||||
case 'remove': {
|
case 'remove': {
|
||||||
// Extra confirmation for destructive 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.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([
|
const { confirm } = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -731,7 +731,7 @@ class ModuleManager {
|
||||||
async compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir, installer = null) {
|
async compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir, installer = null) {
|
||||||
const sourceAgentsPath = path.join(sourcePath, 'agents');
|
const sourceAgentsPath = path.join(sourcePath, 'agents');
|
||||||
const targetAgentsPath = path.join(targetPath, 'agents');
|
const targetAgentsPath = path.join(targetPath, 'agents');
|
||||||
const cfgAgentsDir = path.join(bmadDir, '_bmad', '_config', 'agents');
|
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
|
||||||
|
|
||||||
// Check if agents directory exists in source
|
// Check if agents directory exists in source
|
||||||
if (!(await fs.pathExists(sourceAgentsPath))) {
|
if (!(await fs.pathExists(sourceAgentsPath))) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue